feat: 新增登录页面组件及其样式。
This commit is contained in:
@@ -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,13 +25,15 @@
|
|||||||
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%),
|
||||||
@@ -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: "";
|
||||||
|
|||||||
@@ -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' }}
|
||||||
|
|||||||
Reference in New Issue
Block a user