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;
}
/* ... existing styles ... */
/* ===================== PAGE LAYOUT & BACKGROUND ===================== */
.login-page {
min-height: 100vh;
@@ -23,6 +25,8 @@
color: var(--text-primary);
position: relative;
overflow: hidden;
perspective: 1200px;
/* Enable 3D space */
/* Dynamic Mesh Gradient Background */
background-color: #f8fafc;
@@ -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: "";

View File

@@ -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' }}