feat: 新增登录页面组件及其样式。
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
--error-color: #ef4444;
|
||||
}
|
||||
|
||||
/* ... existing styles ... */
|
||||
|
||||
/* ===================== PAGE LAYOUT & BACKGROUND ===================== */
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
@@ -23,14 +25,16 @@
|
||||
color: var(--text-primary);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
perspective: 1200px;
|
||||
/* Enable 3D space */
|
||||
|
||||
/* Dynamic Mesh Gradient Background */
|
||||
background-color: #f8fafc;
|
||||
background-image:
|
||||
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 100% 0%, hsla(339,49%,30%,1) 0, transparent 50%);
|
||||
background:
|
||||
background-image:
|
||||
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 100% 0%, hsla(339, 49%, 30%, 1) 0, transparent 50%);
|
||||
background:
|
||||
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% 100%, rgba(236, 72, 153, 0.15) 0px, transparent 50%),
|
||||
@@ -38,6 +42,33 @@
|
||||
#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 */
|
||||
.login-page::before {
|
||||
content: "";
|
||||
@@ -72,20 +103,20 @@
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
padding: 3rem;
|
||||
|
||||
|
||||
/* Advanced Glassmorphism */
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: 32px;
|
||||
|
||||
box-shadow:
|
||||
|
||||
box-shadow:
|
||||
0 4px 6px -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),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.6);
|
||||
|
||||
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -106,15 +137,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
|
||||
border-radius: 24px;
|
||||
color: #6366f1;
|
||||
box-shadow:
|
||||
box-shadow:
|
||||
8px 8px 16px rgba(99, 102, 241, 0.1),
|
||||
-8px -8px 16px rgba(255, 255, 255, 0.8),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
|
||||
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -187,13 +218,13 @@
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
color: var(--text-primary);
|
||||
|
||||
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
|
||||
|
||||
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),
|
||||
inset 0 2px 4px rgba(0, 0, 0, 0.01);
|
||||
}
|
||||
@@ -211,7 +242,7 @@
|
||||
outline: none;
|
||||
background: white;
|
||||
border-color: #a855f7;
|
||||
box-shadow:
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(168, 85, 247, 0.1),
|
||||
0 2px 6px rgba(168, 85, 247, 0.05);
|
||||
transform: translateY(-1px);
|
||||
@@ -290,7 +321,7 @@
|
||||
color: white;
|
||||
background: var(--primary-gradient);
|
||||
background-size: 200% auto;
|
||||
box-shadow:
|
||||
box-shadow:
|
||||
0 4px 12px rgba(99, 102, 241, 0.3),
|
||||
0 1px 2px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
@@ -298,7 +329,7 @@
|
||||
.login-button--primary:hover:not(:disabled) {
|
||||
background-position: right center;
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
box-shadow:
|
||||
0 8px 20px rgba(99, 102, 241, 0.4),
|
||||
0 4px 8px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
@@ -382,25 +413,25 @@
|
||||
.login-page {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.login-container {
|
||||
padding: 2rem 1.5rem;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
|
||||
.login-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.login-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
|
||||
.login-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
|
||||
.login-social-buttons {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate, Link, useLocation } from 'react-router-dom';
|
||||
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 './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 (
|
||||
<div className="login-page">
|
||||
<Particles />
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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"
|
||||
>
|
||||
<div className="login-header">
|
||||
<motion.div
|
||||
className="login-logo"
|
||||
whileHover={{ scale: 1.05, rotate: 5 }}
|
||||
whileHover={{ scale: 1.05, rotate: 5, z: 50 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
style={{ transformStyle: "preserve-3d" }}
|
||||
>
|
||||
<Icon icon="solar:wallet-2-bold-duotone" width="36" height="36" />
|
||||
</motion.div>
|
||||
@@ -122,6 +208,7 @@ export default function Login({ mode = 'login' }: LoginProps) {
|
||||
animate={{ opacity: 1, height: 'auto', marginBottom: 16 }}
|
||||
exit={{ opacity: 0, height: 0, marginBottom: 0 }}
|
||||
className="login-error"
|
||||
style={{ transformStyle: "preserve-3d", translateZ: 20 }}
|
||||
>
|
||||
<Icon icon="solar:danger-circle-bold-duotone" width="20" />
|
||||
{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" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
|
||||
Reference in New Issue
Block a user