feat: 新增登录页面组件及其样式。

This commit is contained in:
2026-01-30 08:53:38 +08:00
parent dd14df47a9
commit 596bbe19e8
2 changed files with 172 additions and 26 deletions

View File

@@ -12,6 +12,8 @@
--error-color: #ef4444; --error-color: #ef4444;
} }
/* ... existing styles ... */
/* ===================== PAGE LAYOUT & BACKGROUND ===================== */ /* ===================== PAGE LAYOUT & BACKGROUND ===================== */
.login-page { .login-page {
min-height: 100vh; min-height: 100vh;
@@ -23,14 +25,16 @@
color: var(--text-primary); color: var(--text-primary);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
perspective: 1200px;
/* Enable 3D space */
/* Dynamic Mesh Gradient Background */ /* Dynamic Mesh Gradient Background */
background-color: #f8fafc; background-color: #f8fafc;
background-image: background-image:
radial-gradient(at 0% 0%, hsla(253,16%,7%,1) 0, transparent 50%), radial-gradient(at 0% 0%, hsla(253, 16%, 7%, 1) 0, transparent 50%),
radial-gradient(at 50% 0%, hsla(225,39%,30%,1) 0, transparent 50%), radial-gradient(at 50% 0%, hsla(225, 39%, 30%, 1) 0, transparent 50%),
radial-gradient(at 100% 0%, hsla(339,49%,30%,1) 0, transparent 50%); radial-gradient(at 100% 0%, hsla(339, 49%, 30%, 1) 0, transparent 50%);
background: background:
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%), radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%),
radial-gradient(at 100% 0%, rgba(168, 85, 247, 0.15) 0px, transparent 50%), radial-gradient(at 100% 0%, rgba(168, 85, 247, 0.15) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(236, 72, 153, 0.15) 0px, transparent 50%), radial-gradient(at 100% 100%, rgba(236, 72, 153, 0.15) 0px, transparent 50%),
@@ -38,6 +42,33 @@
#f8fafc; #f8fafc;
} }
/* ===================== STRENGTH METER ===================== */
.password-strength-meter {
margin-top: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.strength-bars {
display: flex;
gap: 0.25rem;
}
.strength-bar {
height: 4px;
flex: 1;
border-radius: 2px;
transition: all 0.3s ease;
}
.strength-text {
font-size: 0.75rem;
font-weight: 600;
text-align: right;
transition: color 0.3s ease;
}
/* Noise Texture Overlay */ /* Noise Texture Overlay */
.login-page::before { .login-page::before {
content: ""; content: "";
@@ -72,20 +103,20 @@
width: 100%; width: 100%;
max-width: 460px; max-width: 460px;
padding: 3rem; padding: 3rem;
/* Advanced Glassmorphism */ /* Advanced Glassmorphism */
background: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px) saturate(180%); backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.8); border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 32px; border-radius: 32px;
box-shadow: box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.02), 0 4px 6px -1px rgba(0, 0, 0, 0.02),
0 2px 4px -1px rgba(0, 0, 0, 0.02), 0 2px 4px -1px rgba(0, 0, 0, 0.02),
0 20px 40px -10px rgba(0, 0, 0, 0.08), 0 20px 40px -10px rgba(0, 0, 0, 0.08),
inset 0 0 0 1px rgba(255, 255, 255, 0.6); inset 0 0 0 1px rgba(255, 255, 255, 0.6);
position: relative; position: relative;
z-index: 10; z-index: 10;
} }
@@ -106,15 +137,15 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%); background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
border-radius: 24px; border-radius: 24px;
color: #6366f1; color: #6366f1;
box-shadow: box-shadow:
8px 8px 16px rgba(99, 102, 241, 0.1), 8px 8px 16px rgba(99, 102, 241, 0.1),
-8px -8px 16px rgba(255, 255, 255, 0.8), -8px -8px 16px rgba(255, 255, 255, 0.8),
inset 0 1px 0 rgba(255, 255, 255, 0.5); inset 0 1px 0 rgba(255, 255, 255, 0.5);
cursor: pointer; cursor: pointer;
z-index: 2; z-index: 2;
} }
@@ -187,13 +218,13 @@
font-size: 1rem; font-size: 1rem;
font-family: inherit; font-family: inherit;
color: var(--text-primary); color: var(--text-primary);
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8);
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 16px; border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: box-shadow:
0 1px 2px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.05),
inset 0 2px 4px rgba(0, 0, 0, 0.01); inset 0 2px 4px rgba(0, 0, 0, 0.01);
} }
@@ -211,7 +242,7 @@
outline: none; outline: none;
background: white; background: white;
border-color: #a855f7; border-color: #a855f7;
box-shadow: box-shadow:
0 0 0 4px rgba(168, 85, 247, 0.1), 0 0 0 4px rgba(168, 85, 247, 0.1),
0 2px 6px rgba(168, 85, 247, 0.05); 0 2px 6px rgba(168, 85, 247, 0.05);
transform: translateY(-1px); transform: translateY(-1px);
@@ -290,7 +321,7 @@
color: white; color: white;
background: var(--primary-gradient); background: var(--primary-gradient);
background-size: 200% auto; background-size: 200% auto;
box-shadow: box-shadow:
0 4px 12px rgba(99, 102, 241, 0.3), 0 4px 12px rgba(99, 102, 241, 0.3),
0 1px 2px rgba(99, 102, 241, 0.2); 0 1px 2px rgba(99, 102, 241, 0.2);
} }
@@ -298,7 +329,7 @@
.login-button--primary:hover:not(:disabled) { .login-button--primary:hover:not(:disabled) {
background-position: right center; background-position: right center;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: box-shadow:
0 8px 20px rgba(99, 102, 241, 0.4), 0 8px 20px rgba(99, 102, 241, 0.4),
0 4px 8px rgba(99, 102, 241, 0.2); 0 4px 8px rgba(99, 102, 241, 0.2);
} }
@@ -382,25 +413,25 @@
.login-page { .login-page {
padding: 1rem; padding: 1rem;
} }
.login-container { .login-container {
padding: 2rem 1.5rem; padding: 2rem 1.5rem;
border-radius: 24px; border-radius: 24px;
} }
.login-header { .login-header {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.login-title { .login-title {
font-size: 1.75rem; font-size: 1.75rem;
} }
.login-logo { .login-logo {
width: 64px; width: 64px;
height: 64px; height: 64px;
} }
.login-social-buttons { .login-social-buttons {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useNavigate, Link, useLocation } from 'react-router-dom'; import { useNavigate, Link, useLocation } from 'react-router-dom';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence, useMotionValue, useTransform, useSpring } from 'framer-motion';
import authService from '../../services/authService'; import authService from '../../services/authService';
import './Login.css'; import './Login.css';
@@ -68,19 +68,105 @@ export default function Login({ mode = 'login' }: LoginProps) {
} }
}; };
// 3D Tilt Logic
const x = useMotionValue(0);
const y = useMotionValue(0);
const mouseX = useSpring(x, { stiffness: 150, damping: 15 });
const mouseY = useSpring(y, { stiffness: 150, damping: 15 });
const rotateX = useTransform(mouseY, [-0.5, 0.5], ["7deg", "-7deg"]);
const rotateY = useTransform(mouseX, [-0.5, 0.5], ["-7deg", "7deg"]);
const handleMouseMove = (e: React.MouseEvent) => {
const rect = e.currentTarget.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
const mouseXFromCenter = e.clientX - rect.left - width / 2;
const mouseYFromCenter = e.clientY - rect.top - height / 2;
x.set(mouseXFromCenter / width);
y.set(mouseYFromCenter / height);
};
const handleMouseLeave = () => {
x.set(0);
y.set(0);
};
// Password Strength Logic
const calculateStrength = (pwd: string) => {
let score = 0;
if (!pwd) return 0;
if (pwd.length > 7) score += 1;
if (pwd.match(/[a-z]/) && pwd.match(/[A-Z]/)) score += 1;
if (pwd.match(/\d/)) score += 1;
if (pwd.match(/[^a-zA-Z\d]/)) score += 1;
return score;
};
const strength = calculateStrength(formData.password);
const strengthColors = ['#e2e8f0', '#ef4444', '#f59e0b', '#3b82f6', '#10b981'];
const strengthLabels = ['未输入', '弱', '中', '强', '极强'];
const Particles = () => {
// Simple random particles
return (
<div className="particles-container" style={{ position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none', zIndex: 0 }}>
{[...Array(20)].map((_, i) => (
<motion.div
key={i}
initial={{
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
opacity: Math.random() * 0.5 + 0.1,
scale: Math.random() * 0.5 + 0.5,
}}
animate={{
y: [null, Math.random() * window.innerHeight],
x: [null, Math.random() * window.innerWidth],
}}
transition={{
duration: Math.random() * 20 + 20,
repeat: Infinity,
repeatType: "reverse",
ease: "linear",
}}
style={{
position: 'absolute',
width: Math.random() * 4 + 2 + 'px', // 2-6px size
height: Math.random() * 4 + 2 + 'px',
borderRadius: '50%',
backgroundColor: i % 2 === 0 ? '#6366f1' : '#ec4899', // Theme colors
filter: 'blur(1px)',
}}
/>
))}
</div>
);
};
return ( return (
<div className="login-page"> <div className="login-page">
<Particles />
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }} transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
style={{
rotateX,
rotateY,
transformStyle: "preserve-3d",
perspective: 1000
}}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="login-container" className="login-container"
> >
<div className="login-header"> <div className="login-header">
<motion.div <motion.div
className="login-logo" className="login-logo"
whileHover={{ scale: 1.05, rotate: 5 }} whileHover={{ scale: 1.05, rotate: 5, z: 50 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
style={{ transformStyle: "preserve-3d" }}
> >
<Icon icon="solar:wallet-2-bold-duotone" width="36" height="36" /> <Icon icon="solar:wallet-2-bold-duotone" width="36" height="36" />
</motion.div> </motion.div>
@@ -122,6 +208,7 @@ export default function Login({ mode = 'login' }: LoginProps) {
animate={{ opacity: 1, height: 'auto', marginBottom: 16 }} animate={{ opacity: 1, height: 'auto', marginBottom: 16 }}
exit={{ opacity: 0, height: 0, marginBottom: 0 }} exit={{ opacity: 0, height: 0, marginBottom: 0 }}
className="login-error" className="login-error"
style={{ transformStyle: "preserve-3d", translateZ: 20 }}
> >
<Icon icon="solar:danger-circle-bold-duotone" width="20" /> <Icon icon="solar:danger-circle-bold-duotone" width="20" />
{error} {error}
@@ -200,8 +287,36 @@ export default function Login({ mode = 'login' }: LoginProps) {
{showPassword ? <Icon icon="solar:eye-closed-bold-duotone" width="18" /> : <Icon icon="solar:eye-bold-duotone" width="18" />} {showPassword ? <Icon icon="solar:eye-closed-bold-duotone" width="18" /> : <Icon icon="solar:eye-bold-duotone" width="18" />}
</button> </button>
</div> </div>
<AnimatePresence> <AnimatePresence>
{!isLogin && ( {!isLogin && formData.password.length > 0 && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="password-strength-meter"
>
<div className="strength-bars">
{[1, 2, 3, 4].map((i) => (
<div
key={i}
className="strength-bar"
style={{
backgroundColor: strength >= i ? strengthColors[strength] : '#e2e8f0',
opacity: strength >= i ? 1 : 0.4
}}
/>
))}
</div>
<span className="strength-text" style={{ color: strengthColors[strength] }}>
{strengthLabels[strength]}
</span>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{!isLogin && formData.password.length === 0 && (
<motion.span <motion.span
initial={{ opacity: 0, height: 0 }} initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }} animate={{ opacity: 1, height: 'auto' }}