babyhealth/pages/login/index.vue
2026-02-06 20:21:10 +08:00

928 lines
20 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>
<view class="login-page">
<!-- 背景装饰 -->
<view class="bg-decoration">
<view class="gradient-orb orb-1"></view>
<view class="gradient-orb orb-2"></view>
<view class="gradient-orb orb-3"></view>
<view class="floating-shapes">
<view class="shape shape-1"></view>
<view class="shape shape-2"></view>
<view class="shape shape-3"></view>
</view>
</view>
<!-- 主要内容 -->
<view class="login-container">
<!-- 头部区域 -->
<view class="header-section">
<view class="logo-container">
<view class="logo-wrapper">
<image src="/static/logo.png" class="logo" mode="aspectFit"></image>
<view class="logo-ring"></view>
</view>
</view>
<view class="welcome-content">
<text class="app-title">企业办公系统</text>
<text class="welcome-subtitle">欢迎回来开始您的工作之旅</text>
</view>
</view>
<!-- 登录卡片 -->
<view class="login-card">
<view class="card-header">
<text class="card-title">登录账户</text>
<view class="card-subtitle">请输入您的登录信息</view>
</view>
<view class="form-container">
<!-- 用户名输入 -->
<view class="input-field-group">
<view class="input-container" :class="{ 'focused': usernameFocused, 'error': usernameError }">
<view class="input-icon-wrapper">
<i class="fas fa-user input-icon"></i>
</view>
<input
v-model="form.username"
placeholder="用户名"
class="input"
type="text"
@focus="handleUsernameFocus"
@blur="handleUsernameBlur"
@input="clearUsernameError">
<view class="input-border"></view>
</view>
<text class="error-text" v-if="usernameError">{{ usernameError }}</text>
</view>
<!-- 密码输入 -->
<view class="input-field-group">
<view class="input-container" :class="{ 'focused': passwordFocused, 'error': passwordError }">
<view class="input-icon-wrapper">
<i class="fas fa-lock input-icon"></i>
</view>
<input
v-model="form.password"
placeholder="密码"
class="input"
:type="showPassword ? 'text' : 'password'"
@focus="handlePasswordFocus"
@blur="handlePasswordBlur"
@input="clearPasswordError">
<view class="password-toggle" @click="togglePassword">
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
</view>
<view class="input-border"></view>
</view>
<text class="error-text" v-if="passwordError">{{ passwordError }}</text>
</view>
<!-- 选项区域 -->
<view class="options-row">
<view class="remember-section" @click="toggleRemember">
<view class="custom-checkbox" :class="{ 'checked': rememberMe }">
<i class="fas fa-check" v-if="rememberMe"></i>
</view>
<text class="remember-text">记住我</text>
</view>
<text class="forgot-link" @click="handleForgotPassword">忘记密码?</text>
</view>
<!-- 登录按钮 -->
<button
:disabled="loading || !isFormValid"
class="login-button"
:class="{ 'loading': loading, 'disabled': !isFormValid }"
@click="handleLogin">
<view class="button-content">
<view class="button-icon" v-if="!loading">
<i class="fas fa-arrow-right"></i>
</view>
<view class="loading-spinner" v-if="loading">
<i class="fas fa-spinner fa-spin"></i>
</view>
<text class="button-text">{{ loading ? '登录中...' : '立即登录' }}</text>
</view>
<view class="button-shine" v-if="!loading"></view>
</button>
</view>
<!-- 测试提示 -->
<view class="test-tips">
<view class="tips-icon">
<i class="fas fa-info-circle"></i>
</view>
<text class="tips-text">测试账号test / 123456</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { userApi } from '../../src/api/index.js'
import { useAuthStore } from '../../src/store/authStore.js'
import { redirectAfterLogin } from '../../src/utils/routeGuard.js'
export default {
data() {
return {
form: {
username: '',
password: ''
},
loading: false,
usernameFocused: false,
passwordFocused: false,
showPassword: false,
rememberMe: false,
usernameError: '',
passwordError: ''
}
},
computed: {
isFormValid() {
return this.form.username.trim() && this.form.password.trim()
}
},
methods: {
async handleLogin() {
this.loading = true;
try {
const res = await userApi.login(this.form);
console.log('登录响应:', res);
// 根据你的后端返回结构调整
const token = res.accessToken || res.token;
if (!token) {
throw new Error('登录失败:未获取到访问令牌');
}
const authStore = useAuthStore();
// 从响应中获取用户信息
const userInfo = res.user || {
username: this.form.username,
id: res.id
};
authStore.login(userInfo, token);
uni.showToast({
title: '登录成功',
icon: 'success'
});
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
redirectAfterLogin();
}, 500);
} catch (e) {
this.loading = false;
console.error('登录错误:', e);
// 显示错误信息
const errorMessage = e.message || e.msg || '登录失败,请检查网络连接';
uni.showToast({
title: errorMessage,
icon: 'none',
duration: 3000
});
}
},
// async handleLogin2() {
// if(!this.form.username || !this.form.password) {
// uni.showToast({
// title: '请输入用户名和密码',
// icon: 'none'
// });
// return;
// }
// this.loading = true;
// try {
// // 模拟登录API请求
// // 实际项目中应该调用真实的后端API
// const mockLogin = () => {
// return new Promise((resolve) => {
// setTimeout(() => {
// // 模拟登录成功
// if (this.form.username === 'admin' && this.form.password === '123456') {
// resolve({
// code: 0,
// message: '登录成功',
// data: {
// token: 'mock_token_' + Date.now(),
// userInfo: {
// id: 1,
// username: this.form.username,
// name: '管理员',
// avatar: '/static/logo.png',
// department: '技术部',
// role: 'admin'
// }
// }
// });
// } else {
// resolve({
// code: 1,
// message: '用户名或密码错误'
// });
// }
// }, 1000);
// });
// };
// const result = await mockLogin();
// if (result.code === 0) {
// // 使用 Pinia store 管理登录状态
// const authStore = useAuthStore();
// authStore.login(result.data.userInfo, result.data.token);
// uni.showToast({
// title: '登录成功',
// icon: 'success'
// });
// // 延迟跳转,让用户看到成功提示
// setTimeout(() => {
// redirectAfterLogin();
// }, 500);
// } else {
// uni.showToast({
// title: result.message || '登录失败',
// icon: 'none'
// });
// }
// } catch (error) {
// console.error('登录错误:', error);
// uni.showToast({
// title: '网络异常,请稍后重试',
// icon: 'none'
// });
// }
// this.loading = false;
// },
// 切换密码显示
togglePassword() {
this.showPassword = !this.showPassword
},
// 切换记住我
toggleRemember() {
this.rememberMe = !this.rememberMe
},
// 忘记密码
handleForgotPassword() {
uni.showToast({
title: '请联系管理员重置密码',
icon: 'none'
})
},
// 处理用户名输入框焦点
handleUsernameFocus() {
this.usernameFocused = true;
this.clearUsernameError();
},
handleUsernameBlur() {
this.usernameFocused = false;
this.validateUsername();
},
// 处理密码输入框焦点
handlePasswordFocus() {
this.passwordFocused = true;
this.clearPasswordError();
},
handlePasswordBlur() {
this.passwordFocused = false;
this.validatePassword();
},
// 清除用户名错误
clearUsernameError() {
this.usernameError = '';
},
// 清除密码错误
clearPasswordError() {
this.passwordError = '';
},
// 验证用户名
validateUsername() {
if (!this.form.username.trim()) {
this.usernameError = '请输入用户名';
return false;
}
return true;
},
// 验证密码
validatePassword() {
if (!this.form.password.trim()) {
this.passwordError = '请输入密码';
return false;
}
if (this.form.password.length < 6) {
this.passwordError = '密码至少6位';
return false;
}
return true;
}
}
}
</script>
<style scoped>
/* 主容器 */
.login-page {
min-height: 100vh;
background: var(--gradient-primary);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 20rpx;
}
/* 背景装饰 */
.bg-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.gradient-orb {
position: absolute;
border-radius: 50%;
background: linear-gradient(45deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
animation: float 8s ease-in-out infinite;
filter: blur(1rpx);
}
.orb-1 {
width: 300rpx;
height: 300rpx;
top: -150rpx;
left: -150rpx;
animation-delay: 0s;
}
.orb-2 {
width: 200rpx;
height: 200rpx;
top: 20%;
right: -100rpx;
animation-delay: 3s;
}
.orb-3 {
width: 150rpx;
height: 150rpx;
bottom: 10%;
left: 10%;
animation-delay: 6s;
}
.floating-shapes {
position: absolute;
width: 100%;
height: 100%;
}
.shape {
position: absolute;
background: rgba(255, 255, 255, 0.05);
animation: float 6s ease-in-out infinite;
}
.shape-1 {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
top: 30%;
left: 20%;
animation-delay: 1s;
}
.shape-2 {
width: 40rpx;
height: 40rpx;
border-radius: 8rpx;
top: 60%;
right: 30%;
animation-delay: 4s;
}
.shape-3 {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
bottom: 30%;
right: 20%;
animation-delay: 2s;
}
@keyframes float {
0%, 100% {
transform: translateY(0px) rotate(0deg) scale(1);
opacity: 0.7;
}
50% {
transform: translateY(-30px) rotate(180deg) scale(1.1);
opacity: 1;
}
}
/* 登录容器 */
.login-container {
width: 100%;
max-width: 600rpx;
position: relative;
z-index: 2;
}
/* 头部区域 */
.header-section {
text-align: center;
margin-bottom: 60rpx;
}
.logo-container {
margin-bottom: 40rpx;
}
.logo-wrapper {
position: relative;
display: inline-block;
}
.logo {
width: 100rpx;
height: 100rpx;
border-radius: 20rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
position: relative;
z-index: 2;
}
.logo-ring {
position: absolute;
top: -8rpx;
left: -8rpx;
right: -8rpx;
bottom: -8rpx;
border: 2rpx solid rgba(255, 255, 255, 0.3);
border-radius: 28rpx;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 0.7;
}
50% {
transform: scale(1.05);
opacity: 1;
}
}
.welcome-content {
color: #ffffff;
}
.app-title {
display: block;
font-size: 48rpx;
font-weight: 700;
margin-bottom: 16rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
letter-spacing: 1rpx;
}
.welcome-subtitle {
display: block;
font-size: 28rpx;
opacity: 0.9;
font-weight: 400;
}
/* 登录卡片 */
.login-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 24rpx;
padding: 50rpx 40rpx;
box-shadow: var(--shadow-lg);
backdrop-filter: blur(20rpx);
border: 1rpx solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
}
.login-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
background: var(--gradient-primary);
}
.card-header {
text-align: center;
margin-bottom: 40rpx;
}
.card-title {
font-size: 36rpx;
font-weight: 700;
color: var(--text-color);
margin-bottom: 8rpx;
display: block;
}
.card-subtitle {
font-size: 26rpx;
color: var(--text-secondary);
font-weight: 400;
}
/* 表单容器 */
.form-container {
margin-bottom: 30rpx;
}
/* 输入框组 */
.input-field-group {
margin-bottom: 32rpx;
}
.input-container {
position: relative;
display: flex;
align-items: center;
background: var(--gray-lighter);
border-radius: 16rpx;
border: 2rpx solid var(--border);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
min-height: 88rpx;
}
.input-container.focused {
border-color: var(--primary-color);
background: var(--white);
box-shadow: 0 0 0 4rpx var(--info-light);
transform: translateY(-2rpx);
}
.input-container.error {
border-color: var(--error);
background: var(--error-light);
}
.input-icon-wrapper {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
}
.input-icon {
color: var(--text-muted);
font-size: 28rpx;
transition: color 0.3s ease;
}
.input-container.focused .input-icon {
color: var(--primary-color);
}
.input-container.error .input-icon {
color: var(--error);
}
.input {
flex: 1;
padding: 24rpx 20rpx;
border: none;
background: transparent;
font-size: 30rpx;
color: var(--text-color);
outline: none;
font-weight: 500;
}
.input::placeholder {
color: var(--text-muted);
font-weight: 400;
}
.password-toggle {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 28rpx;
cursor: pointer;
transition: color 0.3s ease;
margin-right: 20rpx;
}
.password-toggle:hover {
color: var(--primary-color);
}
.input-border {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2rpx;
background: var(--gradient-primary);
transform: scaleX(0);
transition: transform 0.3s ease;
}
.input-container.focused .input-border {
transform: scaleX(1);
}
.error-text {
font-size: 24rpx;
color: var(--error);
margin-top: 8rpx;
margin-left: 20rpx;
display: block;
}
/* 选项区域 */
.options-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
}
.remember-section {
display: flex;
align-items: center;
cursor: pointer;
}
.custom-checkbox {
width: 36rpx;
height: 36rpx;
border: 2rpx solid var(--border);
border-radius: 8rpx;
margin-right: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
background: var(--white);
}
.custom-checkbox.checked {
background: var(--primary-color);
border-color: var(--primary-color);
color: var(--white);
}
.custom-checkbox i {
font-size: 20rpx;
}
.remember-text {
font-size: 28rpx;
color: var(--text-color);
font-weight: 500;
}
.forgot-link {
font-size: 28rpx;
color: var(--primary-color);
font-weight: 500;
cursor: pointer;
transition: color 0.3s ease;
}
.forgot-link:hover {
color: var(--primary-dark);
}
/* 登录按钮 */
.login-button {
width: 100%;
height: 88rpx;
background: var(--gradient-primary);
color: var(--white);
border: none;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
box-shadow: var(--shadow-lg);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.login-button:hover:not(:disabled) {
transform: translateY(-2rpx);
box-shadow: var(--shadow-lg);
}
.login-button:active:not(:disabled) {
transform: translateY(0);
box-shadow: var(--shadow-md);
}
.login-button.disabled {
opacity: 0.5;
transform: none;
cursor: not-allowed;
background: var(--gray);
color: var(--text-muted);
box-shadow: none;
}
.login-button.loading {
pointer-events: none;
}
.button-content {
display: flex;
align-items: center;
gap: 16rpx;
position: relative;
z-index: 2;
}
.button-icon {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
}
.loading-spinner {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
}
.button-text {
font-weight: 600;
}
.button-shine {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: shine 2s infinite;
}
@keyframes shine {
0% { left: -100%; }
100% { left: 100%; }
}
/* 测试提示 */
.test-tips {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
margin-top: 30rpx;
padding: 20rpx;
background: var(--info-light);
border-radius: 12rpx;
border: 1rpx solid var(--primary-light);
}
.tips-icon {
color: var(--primary-color);
font-size: 24rpx;
}
.tips-text {
font-size: 24rpx;
color: var(--text-color);
font-weight: 500;
}
/* 响应式设计 */
@media screen and (max-width: 750rpx) {
.login-container {
max-width: 95%;
}
.login-card {
padding: 40rpx 30rpx;
}
.app-title {
font-size: 42rpx;
}
.welcome-subtitle {
font-size: 26rpx;
}
.card-title {
font-size: 32rpx;
}
.card-subtitle {
font-size: 24rpx;
}
}
/* 深色模式适配 - 保持亮色主题 */
@media (prefers-color-scheme: dark) {
.login-page {
background: var(--gradient-primary);
}
.login-card {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(255, 255, 255, 0.2);
}
.card-title {
color: var(--text-color);
}
.card-subtitle {
color: var(--text-secondary);
}
.input-container {
background: var(--gray-lighter);
border-color: var(--border);
}
.input-container.focused {
background: var(--white);
}
.input {
color: var(--text-color);
}
.input::placeholder {
color: var(--text-muted);
}
.remember-text {
color: var(--text-color);
}
.forgot-link {
color: var(--primary-color);
}
.test-tips {
background: var(--info-light);
border-color: var(--primary-light);
}
.tips-text {
color: var(--text-color);
}
}
</style>