babyhealth/pages/login/index.vue

895 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>
<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: accountFocused, error: accountError }"
>
<view class="input-icon-wrapper">
<i class="fas fa-user input-icon"></i>
</view>
<input
v-model="form.account"
placeholder="用户名"
class="input"
type="text"
@focus="handleAccountFocus"
@blur="handleAccountBlur"
@input="clearAccountError"
/>
<view class="input-border"></view>
</view>
<text class="error-text" v-if="accountError">{{
accountError
}}</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 lang="ts" setup>
import { reactive, ref, computed } from "vue";
import loginApi from "@/src/api/login";
import { useAuthStore } from "@/src/store/authStore.js";
import { redirectAfterLogin } from "@/src/utils/routeGuard.js";
interface LoginForm {
account: string;
password: string;
}
interface LoginUser {
id: number;
account: string;
name: string;
group_id: number;
}
interface LoginResponseData {
token: string;
user?: LoginUser;
id?: number;
}
const form = reactive<LoginForm>({
account: "",
password: "",
});
const loading = ref(false);
const accountFocused = ref(false);
const passwordFocused = ref(false);
const showPassword = ref(false);
const rememberMe = ref(false);
const accountError = ref("");
const passwordError = ref("");
const authStore = useAuthStore();
const isFormValid = computed<boolean>(() => {
return form.account.trim() !== "" && form.password.trim() !== "";
});
async function handleLogin() {
loading.value = true;
try {
const res = (await loginApi.login(form)) as LoginResponseData;
console.log("登录响应:", res);
const { token, user, id } = res || {};
if (!token) {
throw new Error("登录失败:未获取到访问令牌");
}
const userInfo: LoginUser | { account: string; id?: number } = user || {
account: form.account,
id: id,
};
authStore.login(userInfo, token);
uni.showToast({
title: "登录成功",
icon: "success",
});
setTimeout(() => {
redirectAfterLogin();
}, 500);
} catch (e: any) {
loading.value = false;
console.error("登录错误:", e);
const errorMessage: string =
e?.message || e?.msg || "登录失败,请检查网络连接";
uni.showToast({
title: errorMessage,
icon: "none",
duration: 3000,
});
}
}
// 切换密码显示
function togglePassword() {
showPassword.value = !showPassword.value;
}
// 切换记住我
function toggleRemember() {
rememberMe.value = !rememberMe.value;
}
// 忘记密码
function handleForgotPassword() {
uni.showToast({
title: "请联系管理员重置密码",
icon: "none",
});
}
// 处理用户名输入框焦点
function handleAccountFocus() {
accountFocused.value = true;
clearAccountError();
}
function handleAccountBlur() {
accountFocused.value = false;
validateAccount();
}
// 处理密码输入框焦点
function handlePasswordFocus() {
passwordFocused.value = true;
clearPasswordError();
}
function handlePasswordBlur() {
passwordFocused.value = false;
validatePassword();
}
// 清除用户名错误
function clearAccountError() {
accountError.value = "";
}
// 清除密码错误
function clearPasswordError() {
passwordError.value = "";
}
// 验证用户名
function validateAccount(): boolean {
if (!form.account.trim()) {
accountError.value = "请输入用户名";
return false;
}
return true;
}
// 验证密码
function validatePassword(): boolean {
if (!form.password.trim()) {
passwordError.value = "请输入密码";
return false;
}
if (form.password.length < 6) {
passwordError.value = "密码至少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>