fix: ConfirmDialog确认后弹框不关闭(9页面) + 登录页顶部渐变线移除
- 9个页面 onConfirm 回调添加 setDeleteConfirm(null) 关闭弹框 - InstancePage/InstanceDetailPage/InstanceGroupPage/RedisDetailPage - UsersPage/AnnouncementPage/NotificationPage/NotificationsPage/WorkflowPage - 白日梦境(NeonCityView) + 云石纹理(GradientMeshView) 移除顶部蓝色渐变线
This commit is contained in:
1 parent
5a61babd33
commit
4351421848
11 files changed
+272
-513
No files matched your search
@@ -1,307 +1,214 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* KEYFRAMES */
|
||||
/* ------------------------------------------------------------------ */
|
||||
const KEYFRAMES = `
|
||||
@keyframes mesh-drift-1 {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(30px, 20px); }
|
||||
@keyframes orb-1 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
33% { transform: translate(30px, -40px) scale(1.05); }
|
||||
66% { transform: translate(-20px, 20px) scale(0.95); }
|
||||
}
|
||||
@keyframes mesh-drift-2 {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(-25px, -30px); }
|
||||
@keyframes orb-2 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
33% { transform: translate(-35px, 25px) scale(0.97); }
|
||||
66% { transform: translate(25px, -30px) scale(1.03); }
|
||||
}
|
||||
@keyframes mesh-drift-3 {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(20px, -25px); }
|
||||
@keyframes orb-3 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(20px, 35px) scale(1.04); }
|
||||
}
|
||||
@keyframes mesh-drift-4 {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(-30px, 20px); }
|
||||
@keyframes spin-slow {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes mesh-drift-5 {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(25px, 30px); }
|
||||
@keyframes pulse-ring {
|
||||
0% { transform: scale(1); opacity: 0.3; }
|
||||
50% { transform: scale(1.08); opacity: 0.15; }
|
||||
100% { transform: scale(1); opacity: 0.3; }
|
||||
}
|
||||
@keyframes mesh-drift-6 {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(-15px, 25px); }
|
||||
}
|
||||
@keyframes mesh-drift-7 {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(20px, -15px); }
|
||||
@keyframes float-gentle {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
`;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* COMPONENT */
|
||||
/* ------------------------------------------------------------------ */
|
||||
export default function GradientMeshView(): React.JSX.Element {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
const [focusedField, setFocusedField] = useState<string | null>(null);
|
||||
const [hoverBtn, setHoverBtn] = useState(false);
|
||||
|
||||
const [focus, setFocus] = useState<string | null>(null);
|
||||
const [time, setTime] = useState(0);
|
||||
const isReady = username.trim().length > 0 && password.trim().length > 0;
|
||||
|
||||
/* ---- marble / terrazzo blobs (light tones) ---- */
|
||||
const blobs: React.CSSProperties[] = [
|
||||
{
|
||||
position: 'fixed', top: '-120px', left: '-80px',
|
||||
width: '650px', height: '650px',
|
||||
background: 'radial-gradient(circle, rgba(99,102,241,0.06), transparent 70%)',
|
||||
filter: 'blur(90px)', pointerEvents: 'none',
|
||||
animation: 'mesh-drift-1 14s ease-in-out infinite',
|
||||
},
|
||||
{
|
||||
position: 'fixed', bottom: '-100px', right: '-60px',
|
||||
width: '550px', height: '550px',
|
||||
background: 'radial-gradient(circle, rgba(244,63,94,0.04), transparent 70%)',
|
||||
filter: 'blur(100px)', pointerEvents: 'none',
|
||||
animation: 'mesh-drift-2 12s ease-in-out infinite',
|
||||
},
|
||||
{
|
||||
position: 'fixed', top: '35%', right: '8%',
|
||||
width: '450px', height: '450px',
|
||||
background: 'radial-gradient(circle, rgba(16,185,129,0.05), transparent 70%)',
|
||||
filter: 'blur(110px)', pointerEvents: 'none',
|
||||
animation: 'mesh-drift-3 16s ease-in-out infinite',
|
||||
},
|
||||
{
|
||||
position: 'fixed', bottom: '10%', left: '8%',
|
||||
width: '500px', height: '500px',
|
||||
background: 'radial-gradient(circle, rgba(139,92,246,0.04), transparent 70%)',
|
||||
filter: 'blur(95px)', pointerEvents: 'none',
|
||||
animation: 'mesh-drift-4 13s ease-in-out infinite',
|
||||
},
|
||||
{
|
||||
position: 'fixed', top: '-60px', right: '25%',
|
||||
width: '580px', height: '580px',
|
||||
background: 'radial-gradient(circle, rgba(99,102,241,0.04), transparent 70%)',
|
||||
filter: 'blur(100px)', pointerEvents: 'none',
|
||||
animation: 'mesh-drift-5 15s ease-in-out infinite',
|
||||
},
|
||||
{
|
||||
position: 'fixed', top: '60%', left: '30%',
|
||||
width: '400px', height: '400px',
|
||||
background: 'radial-gradient(circle, rgba(244,63,94,0.03), transparent 70%)',
|
||||
filter: 'blur(105px)', pointerEvents: 'none',
|
||||
animation: 'mesh-drift-6 11s ease-in-out infinite',
|
||||
},
|
||||
{
|
||||
position: 'fixed', top: '15%', left: '50%',
|
||||
width: '480px', height: '480px',
|
||||
background: 'radial-gradient(circle, rgba(16,185,129,0.04), transparent 70%)',
|
||||
filter: 'blur(95px)', pointerEvents: 'none',
|
||||
animation: 'mesh-drift-7 17s ease-in-out infinite',
|
||||
},
|
||||
];
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setTime(t => t + 1), 50);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
/* ---- input base style ---- */
|
||||
const inputBase: React.CSSProperties = {
|
||||
width: '100%',
|
||||
background: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: 12,
|
||||
padding: '14px 18px',
|
||||
color: '#111827',
|
||||
fontSize: 15,
|
||||
outline: 'none',
|
||||
transition: 'all 0.25s ease',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
const getInputStyle = (field: string): React.CSSProperties => ({
|
||||
...inputBase,
|
||||
borderColor: focusedField === field ? '#8b5cf6' : '#e5e7eb',
|
||||
boxShadow: focusedField === field
|
||||
? '0 0 0 3px rgba(99,102,241,0.1)'
|
||||
: 'none',
|
||||
});
|
||||
// Animated hue shift for orbs
|
||||
const hue1 = 220 + Math.sin(time * 0.02) * 15;
|
||||
const hue2 = 260 + Math.cos(time * 0.015) * 20;
|
||||
const hue3 = 190 + Math.sin(time * 0.025) * 10;
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{KEYFRAMES}</style>
|
||||
<div style={{ minHeight: '100vh', position: 'relative', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fafaf9', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' }}>
|
||||
|
||||
{/* ===================== ROOT CONTAINER ===================== */}
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#fafaf8',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
}}
|
||||
>
|
||||
{/* Orb 1 - top left */}
|
||||
<div style={{
|
||||
position: 'absolute', top: '-10%', left: '-5%',
|
||||
width: 500, height: 500, borderRadius: '50%',
|
||||
background: `radial-gradient(circle at 40% 40%, hsla(${hue1}, 70%, 65%, 0.12), hsla(${hue2}, 60%, 55%, 0.04) 60%, transparent 70%)`,
|
||||
animation: 'orb-1 20s ease-in-out infinite', filter: 'blur(2px)', pointerEvents: 'none',
|
||||
}} />
|
||||
{/* Orb 2 - bottom right */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: '-8%', right: '-3%',
|
||||
width: 450, height: 450, borderRadius: '50%',
|
||||
background: `radial-gradient(circle at 60% 60%, hsla(${hue2}, 65%, 60%, 0.10), hsla(${hue3}, 55%, 50%, 0.03) 60%, transparent 70%)`,
|
||||
animation: 'orb-2 24s ease-in-out infinite', filter: 'blur(2px)', pointerEvents: 'none',
|
||||
}} />
|
||||
{/* Orb 3 - center right */}
|
||||
<div style={{
|
||||
position: 'absolute', top: '40%', right: '15%',
|
||||
width: 320, height: 320, borderRadius: '50%',
|
||||
background: `radial-gradient(circle at 50% 50%, hsla(${hue3}, 60%, 62%, 0.08), transparent 65%)`,
|
||||
animation: 'orb-3 16s ease-in-out infinite', filter: 'blur(2px)', pointerEvents: 'none',
|
||||
}} />
|
||||
|
||||
{/* ---- marble / terrazzo texture blobs ---- */}
|
||||
{blobs.map((b, i) => (
|
||||
<div key={i} style={b} />
|
||||
))}
|
||||
|
||||
{/* ---- subtle dot-grid texture (light version) ---- */}
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
pointerEvents: 'none',
|
||||
opacity: 0.28,
|
||||
backgroundImage:
|
||||
'radial-gradient(circle, rgba(0,0,0,0.08) 1px, transparent 1px)',
|
||||
backgroundSize: '24px 24px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ===================== LOGIN CARD ===================== */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
margin: '0 20px',
|
||||
background: 'rgba(255,255,255,0.85)',
|
||||
backdropFilter: 'blur(40px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(40px) saturate(180%)',
|
||||
border: 'none',
|
||||
borderRadius: 24,
|
||||
padding: '48px 36px',
|
||||
boxShadow:
|
||||
'0 1px 2px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.06), 0 12px 40px rgba(0,0,0,0.04)',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
|
||||
{/* ---- brand ---- */}
|
||||
<div style={{ textAlign: 'center', marginBottom: 36 }}>
|
||||
{/* Logo */}
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 16,
|
||||
background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 20,
|
||||
boxShadow: '0 6px 20px rgba(99,102,241,0.2)',
|
||||
}}
|
||||
>
|
||||
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2L20 6V11C20 16 16 19.5 12 22C8 19.5 4 16 4 11V6L12 2Z" /><path d="M9 12L11 14L15 10" /></svg>
|
||||
{/* Geometric decoration - rotating ring */}
|
||||
<div style={{ position: 'absolute', top: '12%', right: '10%', width: 180, height: 180, pointerEvents: 'none', opacity: 0.08, animation: 'spin-slow 60s linear infinite' }}>
|
||||
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" stroke="#6366f1" strokeWidth="1">
|
||||
<circle cx="90" cy="90" r="85" strokeDasharray="8 12" />
|
||||
<circle cx="90" cy="90" r="60" strokeDasharray="4 8" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1
|
||||
style={{
|
||||
color: '#111827',
|
||||
fontSize: 24,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.5px',
|
||||
margin: '0 0 8px 0',
|
||||
}}
|
||||
>
|
||||
{/* Subtle grid dots */}
|
||||
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', opacity: 0.18, backgroundImage: 'radial-gradient(circle, rgba(99,102,241,0.15) 1px, transparent 1px)', backgroundSize: '32px 32px' }} />
|
||||
|
||||
{/* Decorative line accents */}
|
||||
<div style={{ position: 'absolute', bottom: '18%', left: '5%', width: 120, height: 1, background: 'linear-gradient(90deg, transparent, rgba(99,102,241,0.15), transparent)', pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', top: '25%', left: '8%', width: 80, height: 1, background: 'linear-gradient(90deg, transparent, rgba(139,92,246,0.12), transparent)', pointerEvents: 'none' }} />
|
||||
|
||||
{/* Login card */}
|
||||
<div style={{
|
||||
position: 'relative', zIndex: 10, width: '100%', maxWidth: 420, margin: '0 20px',
|
||||
background: 'rgba(255,255,255,0.82)', backdropFilter: 'blur(40px) saturate(180%)', WebkitBackdropFilter: 'blur(40px) saturate(180%)',
|
||||
borderRadius: 28, padding: '52px 44px 44px',
|
||||
boxShadow: '0 0 0 1px rgba(255,255,255,0.7), 0 4px 12px rgba(99,102,241,0.04), 0 16px 40px rgba(0,0,0,0.05)',
|
||||
animation: 'float-gentle 5s ease-in-out infinite',
|
||||
}}>
|
||||
|
||||
|
||||
{/* Pulse ring behind logo */}
|
||||
<div style={{ position: 'absolute', top: 28, left: '50%', transform: 'translateX(-50%)', width: 72, height: 72, borderRadius: 22, border: '1.5px solid rgba(99,102,241,0.1)', animation: 'pulse-ring 3s ease-in-out infinite', pointerEvents: 'none' }} />
|
||||
|
||||
{/* Brand */}
|
||||
<div style={{ textAlign: 'center', marginBottom: 40 }}>
|
||||
<div style={{
|
||||
width: 56, height: 56, borderRadius: 18,
|
||||
background: 'linear-gradient(135deg, #3b82f6, #6366f1, #8b5cf6)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', marginBottom: 24,
|
||||
boxShadow: '0 8px 28px rgba(99,102,241,0.2), inset 0 1px 0 rgba(255,255,255,0.2)',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2L20 6V11C20 16 16 19.5 12 22C8 19.5 4 16 4 11V6L12 2Z" /><path d="M9 12L11 14L15 10" /></svg>
|
||||
</div>
|
||||
<h1 style={{ fontSize: 26, fontWeight: 800, color: '#111827', margin: '0 0 6px', letterSpacing: '-0.5px' }}>
|
||||
OpsxSQL
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
color: '#6b7280',
|
||||
fontSize: 14,
|
||||
letterSpacing: '0.3px',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
<p style={{ fontSize: 14, color: '#6b7280', margin: 0, fontWeight: 400 }}>
|
||||
企业级数据库审核与合规管理
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ---- form ---- */}
|
||||
<form
|
||||
onSubmit={handleLogin}
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 14 }}
|
||||
>
|
||||
{/* username */}
|
||||
{/* Form */}
|
||||
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{/* Username */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', left: 16, top: '50%', transform: 'translateY(-50%)', color: focus === 'username' ? '#6366f1' : '#a1a1aa', transition: 'color 0.2s' }}>
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="用户名"
|
||||
value={username}
|
||||
type="text" placeholder="用户名" value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onFocus={() => setFocusedField('username')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
style={getInputStyle('username')}
|
||||
onFocus={() => setFocus('username')} onBlur={() => setFocus(null)}
|
||||
autoComplete="username"
|
||||
/>
|
||||
|
||||
{/* password */}
|
||||
<input
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onFocus={() => setFocusedField('password')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
style={getInputStyle('password')}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
|
||||
{/* error */}
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
background: 'rgba(239,68,68,0.06)',
|
||||
color: '#ef4444',
|
||||
borderRadius: 10,
|
||||
padding: '10px 14px',
|
||||
fontSize: 13,
|
||||
width: '100%', height: 50, padding: '0 16px 0 44px', fontSize: 14, color: '#111827',
|
||||
background: focus === 'username' ? '#fff' : '#f4f4f5',
|
||||
border: `1.5px solid ${focus === 'username' ? '#818cf8' : 'transparent'}`,
|
||||
borderRadius: 14, outline: 'none', boxSizing: 'border-box', transition: 'all 0.2s ease',
|
||||
boxShadow: focus === 'username' ? '0 0 0 4px rgba(99,102,241,0.06)' : 'none',
|
||||
}}
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', left: 16, top: '50%', transform: 'translateY(-50%)', color: focus === 'password' ? '#6366f1' : '#a1a1aa', transition: 'color 0.2s' }}>
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg>
|
||||
</div>
|
||||
<input
|
||||
type="password" placeholder="密码" value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onFocus={() => setFocus('password')} onBlur={() => setFocus(null)}
|
||||
autoComplete="current-password"
|
||||
style={{
|
||||
width: '100%', height: 50, padding: '0 16px 0 44px', fontSize: 14, color: '#111827',
|
||||
background: focus === 'password' ? '#fff' : '#f4f4f5',
|
||||
border: `1.5px solid ${focus === 'password' ? '#818cf8' : 'transparent'}`,
|
||||
borderRadius: 14, outline: 'none', boxSizing: 'border-box', transition: 'all 0.2s ease',
|
||||
boxShadow: focus === 'password' ? '0 0 0 4px rgba(99,102,241,0.06)' : 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: '12px 16px', borderRadius: 12,
|
||||
background: 'linear-gradient(135deg, #fef2f2, #fff1f2)',
|
||||
color: '#dc2626', fontSize: 13,
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><line x1="15" y1="9" x2="9" y2="15" /><line x1="9" y1="9" x2="15" y2="15" /></svg>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* submit */}
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isReady || loading}
|
||||
onMouseEnter={() => setHoverBtn(true)}
|
||||
onMouseLeave={() => setHoverBtn(false)}
|
||||
type="submit" disabled={!isReady || loading}
|
||||
style={{
|
||||
width: '100%',
|
||||
marginTop: 4,
|
||||
padding: '14px 24px',
|
||||
borderRadius: 12,
|
||||
border: 'none',
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
color: '#fff',
|
||||
width: '100%', height: 52, marginTop: 4,
|
||||
padding: '0 24px', borderRadius: 14, border: 'none',
|
||||
fontSize: 15, fontWeight: 700, color: '#fff', letterSpacing: '0.5px',
|
||||
cursor: isReady && !loading ? 'pointer' : 'not-allowed',
|
||||
opacity: isReady ? 1 : 0.5,
|
||||
background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
|
||||
boxShadow: hoverBtn && isReady && !loading
|
||||
? '0 8px 28px rgba(99,102,241,0.35)'
|
||||
: '0 4px 16px rgba(99,102,241,0.2)',
|
||||
transform: hoverBtn && isReady && !loading ? 'translateY(-1px)' : 'translateY(0)',
|
||||
transition: 'all 0.25s ease',
|
||||
background: isReady
|
||||
? 'linear-gradient(135deg, #3b82f6, #6366f1, #8b5cf6)'
|
||||
: '#d4d4d8',
|
||||
boxShadow: isReady
|
||||
? '0 4px 20px rgba(99,102,241,0.25), inset 0 1px 0 rgba(255,255,255,0.15)'
|
||||
: 'none',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => { if (isReady) { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 8px 32px rgba(99,102,241,0.35), inset 0 1px 0 rgba(255,255,255,0.15)'; } }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = isReady ? '0 4px 20px rgba(99,102,241,0.25), inset 0 1px 0 rgba(255,255,255,0.15)' : 'none'; }}
|
||||
>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
{loading ? (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" /><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite" /></svg>
|
||||
登录中...
|
||||
</span>
|
||||
) : '登 录'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* ---- copyright ---- */}
|
||||
<p
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
color: '#9ca3af',
|
||||
fontSize: 12,
|
||||
margin: '28px 0 0 0',
|
||||
}}
|
||||
>
|
||||
© 2025 OpsxSQL
|
||||
</p>
|
||||
{/* Footer */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: 32, opacity: 0.4 }}>
|
||||
<div style={{ width: 12, height: 12, borderRadius: 3, background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)' }} />
|
||||
<span style={{ fontSize: 12, color: '#6b7280' }}>OpsxSQL · 安全合规的数据库管理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,301 +1,153 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
|
||||
const KEYFRAMES = `
|
||||
@keyframes float-slow {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(20px, -30px) scale(1.03); }
|
||||
50% { transform: translate(-10px, -50px) scale(1.01); }
|
||||
75% { transform: translate(15px, -20px) scale(1.04); }
|
||||
@keyframes morph-1 {
|
||||
0%, 100% { border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; transform: translate(0, 0) rotate(0deg); }
|
||||
25% { border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%; transform: translate(15px, -25px) rotate(5deg); }
|
||||
50% { border-radius: 50% 60% 30% 60% / 40% 50% 60% 50%; transform: translate(-10px, 15px) rotate(-3deg); }
|
||||
75% { border-radius: 40% 30% 60% 50% / 60% 40% 50% 70%; transform: translate(20px, -10px) rotate(7deg); }
|
||||
}
|
||||
|
||||
@keyframes float-medium {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(-25px, -40px) scale(1.02); }
|
||||
50% { transform: translate(20px, -60px) scale(0.98); }
|
||||
75% { transform: translate(-15px, -25px) scale(1.03); }
|
||||
@keyframes morph-2 {
|
||||
0%, 100% { border-radius: 40% 60% 70% 30% / 50% 40% 60% 50%; transform: translate(0, 0) rotate(0deg); }
|
||||
33% { border-radius: 70% 30% 50% 60% / 30% 60% 40% 70%; transform: translate(-20px, 20px) rotate(-8deg); }
|
||||
66% { border-radius: 50% 40% 60% 50% / 60% 70% 30% 40%; transform: translate(25px, -15px) rotate(4deg); }
|
||||
}
|
||||
|
||||
@keyframes float-fast {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(15px, -20px) scale(1.01); }
|
||||
50% { transform: translate(-20px, -35px) scale(1.02); }
|
||||
75% { transform: translate(10px, -15px) scale(0.99); }
|
||||
@keyframes morph-3 {
|
||||
0%, 100% { border-radius: 50% 50% 40% 60% / 40% 60% 50% 50%; transform: translate(0, 0) rotate(0deg); }
|
||||
50% { border-radius: 30% 70% 50% 50% / 70% 30% 60% 40%; transform: translate(-15px, -20px) rotate(6deg); }
|
||||
}
|
||||
@keyframes line-flow {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(200%); }
|
||||
}
|
||||
@keyframes float-card {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-6px); }
|
||||
}
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% center; }
|
||||
100% { background-position: 200% center; }
|
||||
}
|
||||
`;
|
||||
|
||||
// Floating shapes: [width, height, top, left, color, opacity, animation, duration]
|
||||
const FLOATING_SHAPES: [number, number, string, string, string, number, string, string][] = [
|
||||
[340, 280, '8%', '5%', 'linear-gradient(135deg, #e0f2fe 0%, #f3e8ff 100%)', 0.5, 'float-slow', '20s'],
|
||||
[240, 200, '55%', '2%', 'linear-gradient(135deg, #f3e8ff 0%, #ede9fe 100%)', 0.4, 'float-medium', '25s'],
|
||||
[180, 160, '70%', '15%', 'linear-gradient(135deg, #dbeafe 0%, #e0f2fe 100%)', 0.35, 'float-fast', '18s'],
|
||||
[280, 240, '20%', '18%', 'linear-gradient(135deg, #ede9fe 0%, #e0f2fe 100%)', 0.3, 'float-slow', '22s'],
|
||||
];
|
||||
|
||||
export default function NeonCityView(): JSX.Element {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
|
||||
const [focus, setFocus] = useState<string | null>(null);
|
||||
const isDisabled = !username || !password || loading;
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{KEYFRAMES}</style>
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(180deg, #fafbfc 0%, #f0f2f5 100%)',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
}}
|
||||
>
|
||||
{/* Floating soft geometric shapes */}
|
||||
{FLOATING_SHAPES.map(([w, h, top, left, bg, opacity, anim, dur], i) => (
|
||||
<div
|
||||
key={`shape-${i}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top,
|
||||
left,
|
||||
width: w,
|
||||
height: h,
|
||||
background: bg,
|
||||
opacity,
|
||||
borderRadius: '50%',
|
||||
filter: 'blur(40px)',
|
||||
animation: `${anim} ${dur} ease-in-out infinite`,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<div style={{ minHeight: '100vh', position: 'relative', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f8f9fc', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' }}>
|
||||
|
||||
{/* Morphing blobs */}
|
||||
<div style={{ position: 'absolute', top: '-8%', left: '-5%', width: 420, height: 420, background: 'linear-gradient(135deg, rgba(99,102,241,0.12), rgba(168,85,247,0.08))', animation: 'morph-1 18s ease-in-out infinite', filter: 'blur(0px)', pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', bottom: '-5%', right: '-3%', width: 380, height: 380, background: 'linear-gradient(135deg, rgba(59,130,246,0.10), rgba(6,182,212,0.08))', animation: 'morph-2 22s ease-in-out infinite', filter: 'blur(0px)', pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', top: '55%', left: '60%', width: 280, height: 280, background: 'linear-gradient(135deg, rgba(236,72,153,0.06), rgba(168,85,247,0.06))', animation: 'morph-3 16s ease-in-out infinite', filter: 'blur(0px)', pointerEvents: 'none' }} />
|
||||
|
||||
{/* Fine grid */}
|
||||
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', opacity: 0.35, backgroundImage: 'linear-gradient(rgba(99,102,241,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(99,102,241,0.04) 1px, transparent 1px)', backgroundSize: '60px 60px' }} />
|
||||
|
||||
{/* Decorative lines */}
|
||||
{[15, 35, 58, 78].map((t, i) => (
|
||||
<div key={`line-${i}`} style={{ position: 'absolute', top: `${t}%`, left: 0, right: 0, height: 1, overflow: 'hidden', pointerEvents: 'none', opacity: 0.08 }}>
|
||||
<div style={{ width: '40%', height: '100%', background: 'linear-gradient(90deg, transparent, #6366f1, transparent)', animation: `line-flow ${6 + i * 2}s linear infinite`, animationDelay: `${i * 1.5}s` }} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Login card */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
zIndex: 10,
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
margin: '0 20px',
|
||||
background: 'rgba(255, 255, 255, 0.72)',
|
||||
backdropFilter: 'blur(24px)',
|
||||
WebkitBackdropFilter: 'blur(24px)',
|
||||
borderRadius: 20,
|
||||
padding: '48px 40px',
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.06)',
|
||||
}}
|
||||
>
|
||||
{/* Brand area */}
|
||||
<div style={{ textAlign: 'center', marginBottom: 36 }}>
|
||||
{/* Logo */}
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 14,
|
||||
background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 20,
|
||||
boxShadow: '0 4px 16px rgba(59, 130, 246, 0.25)',
|
||||
}}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2L20 6V11C20 16 16 19.5 12 22C8 19.5 4 16 4 11V6L12 2Z" /><path d="M9 12L11 14L15 10" /></svg>
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'relative', zIndex: 10, width: '100%', maxWidth: 420, margin: '0 20px',
|
||||
background: 'rgba(255,255,255,0.78)', backdropFilter: 'blur(32px) saturate(180%)', WebkitBackdropFilter: 'blur(32px) saturate(180%)',
|
||||
borderRadius: 24, padding: '52px 44px 44px',
|
||||
boxShadow: '0 0 0 1px rgba(255,255,255,0.6), 0 8px 16px rgba(99,102,241,0.06), 0 24px 48px rgba(0,0,0,0.06)',
|
||||
animation: 'float-card 6s ease-in-out infinite',
|
||||
}}>
|
||||
|
||||
{/* Title */}
|
||||
<h1
|
||||
style={{
|
||||
fontSize: 26,
|
||||
fontWeight: 700,
|
||||
color: '#1a1a2e',
|
||||
margin: '0 0 8px',
|
||||
letterSpacing: '0.3px',
|
||||
}}
|
||||
>
|
||||
{/* Brand */}
|
||||
<div style={{ textAlign: 'center', marginBottom: 40 }}>
|
||||
<div style={{
|
||||
width: 56, height: 56, borderRadius: 18,
|
||||
background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', marginBottom: 24,
|
||||
boxShadow: '0 8px 24px rgba(99,102,241,0.25), inset 0 1px 0 rgba(255,255,255,0.2)',
|
||||
}}>
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2L20 6V11C20 16 16 19.5 12 22C8 19.5 4 16 4 11V6L12 2Z" /><path d="M9 12L11 14L15 10" /></svg>
|
||||
</div>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 800, color: '#0f172a', margin: '0 0 6px', letterSpacing: '-0.5px', background: 'linear-gradient(135deg, #1e293b, #475569)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
|
||||
OpsxSQL
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
margin: 0,
|
||||
letterSpacing: '0.2px',
|
||||
}}
|
||||
>
|
||||
<p style={{ fontSize: 14, color: '#64748b', margin: 0, fontWeight: 400 }}>
|
||||
企业级数据库审核与合规管理
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Login form */}
|
||||
<form onSubmit={handleLogin}>
|
||||
{/* Username input */}
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label
|
||||
htmlFor="username"
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: '#64748b',
|
||||
marginBottom: 8,
|
||||
letterSpacing: '0.2px',
|
||||
}}
|
||||
>
|
||||
用户名
|
||||
</label>
|
||||
{/* Username */}
|
||||
<div style={{ marginBottom: 18 }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', left: 16, top: '50%', transform: 'translateY(-50%)', color: focus === 'username' ? '#6366f1' : '#94a3b8', transition: 'color 0.2s' }}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
|
||||
</div>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="请输入用户名"
|
||||
autoComplete="username"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 46,
|
||||
padding: '0 16px',
|
||||
fontSize: 14,
|
||||
color: '#1a1a2e',
|
||||
background: '#f5f5f5',
|
||||
border: '1.5px solid transparent',
|
||||
borderRadius: 10,
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
transition: 'border-color 0.25s ease, background 0.25s ease',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.borderColor = '#93c5fd';
|
||||
e.currentTarget.style.background = '#fff';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.borderColor = 'transparent';
|
||||
e.currentTarget.style.background = '#f5f5f5';
|
||||
}}
|
||||
type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="请输入用户名" autoComplete="username"
|
||||
onFocus={() => setFocus('username')} onBlur={() => setFocus(null)}
|
||||
style={{ width: '100%', height: 50, padding: '0 16px 0 46px', fontSize: 14, color: '#0f172a', background: focus === 'username' ? '#fff' : '#f1f5f9', border: `1.5px solid ${focus === 'username' ? '#818cf8' : 'transparent'}`, borderRadius: 14, outline: 'none', boxSizing: 'border-box', transition: 'all 0.2s ease', boxShadow: focus === 'username' ? '0 0 0 4px rgba(99,102,241,0.08)' : 'none' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password input */}
|
||||
{/* Password */}
|
||||
<div style={{ marginBottom: 28 }}>
|
||||
<label
|
||||
htmlFor="password"
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: '#64748b',
|
||||
marginBottom: 8,
|
||||
letterSpacing: '0.2px',
|
||||
}}
|
||||
>
|
||||
密码
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', left: 16, top: '50%', transform: 'translateY(-50%)', color: focus === 'password' ? '#6366f1' : '#94a3b8', transition: 'color 0.2s' }}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg>
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="请输入密码"
|
||||
autoComplete="current-password"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 46,
|
||||
padding: '0 16px',
|
||||
fontSize: 14,
|
||||
color: '#1a1a2e',
|
||||
background: '#f5f5f5',
|
||||
border: '1.5px solid transparent',
|
||||
borderRadius: 10,
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
transition: 'border-color 0.25s ease, background 0.25s ease',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.borderColor = '#93c5fd';
|
||||
e.currentTarget.style.background = '#fff';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.borderColor = 'transparent';
|
||||
e.currentTarget.style.background = '#f5f5f5';
|
||||
}}
|
||||
type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="请输入密码" autoComplete="current-password"
|
||||
onFocus={() => setFocus('password')} onBlur={() => setFocus(null)}
|
||||
style={{ width: '100%', height: 50, padding: '0 16px 0 46px', fontSize: 14, color: '#0f172a', background: focus === 'password' ? '#fff' : '#f1f5f9', border: `1.5px solid ${focus === 'password' ? '#818cf8' : 'transparent'}`, borderRadius: 14, outline: 'none', boxSizing: 'border-box', transition: 'all 0.2s ease', boxShadow: focus === 'password' ? '0 0 0 4px rgba(99,102,241,0.08)' : 'none' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
padding: '10px 14px',
|
||||
borderRadius: 10,
|
||||
background: '#fef2f2',
|
||||
color: '#dc2626',
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 16, padding: '12px 16px', borderRadius: 12, background: 'linear-gradient(135deg, #fef2f2, #fff1f2)', color: '#dc2626', fontSize: 13, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><line x1="15" y1="9" x2="9" y2="15" /><line x1="9" y1="9" x2="15" y2="15" /></svg>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Login button */}
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isDisabled}
|
||||
type="submit" disabled={isDisabled}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 48,
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
color: '#fff',
|
||||
background: isDisabled
|
||||
? '#cbd5e1'
|
||||
: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
|
||||
border: 'none',
|
||||
borderRadius: 10,
|
||||
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.25s ease',
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isDisabled) {
|
||||
e.currentTarget.style.transform = 'scale(1.02)';
|
||||
e.currentTarget.style.boxShadow = '0 6px 20px rgba(59, 130, 246, 0.3)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
width: '100%', height: 52, fontSize: 15, fontWeight: 700, color: '#fff',
|
||||
background: isDisabled ? '#cbd5e1' : 'linear-gradient(135deg, #3b82f6, #6366f1, #8b5cf6)',
|
||||
backgroundSize: isDisabled ? '100%' : '200% 100%',
|
||||
border: 'none', borderRadius: 14, cursor: isDisabled ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.3s ease', letterSpacing: '0.5px',
|
||||
boxShadow: isDisabled ? 'none' : '0 4px 16px rgba(99,102,241,0.3), inset 0 1px 0 rgba(255,255,255,0.15)',
|
||||
}}
|
||||
onMouseEnter={(e) => { if (!isDisabled) { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 8px 28px rgba(99,102,241,0.4), inset 0 1px 0 rgba(255,255,255,0.15)'; } }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = isDisabled ? 'none' : '0 4px 16px rgba(99,102,241,0.3), inset 0 1px 0 rgba(255,255,255,0.15)'; }}
|
||||
>
|
||||
{loading ? (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" /><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite" /></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" /><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite" /></svg>
|
||||
登录中...
|
||||
</span>
|
||||
) : (
|
||||
'登 录'
|
||||
)}
|
||||
) : '登 录'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Copyright */}
|
||||
<p
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
fontSize: 12,
|
||||
color: '#94a3b8',
|
||||
marginTop: 32,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
© OpsxSQL · 安全合规的数据库管理
|
||||
</p>
|
||||
{/* Footer */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: 32, opacity: 0.4 }}>
|
||||
<div style={{ width: 12, height: 12, borderRadius: 3, background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)' }} />
|
||||
<span style={{ fontSize: 12, color: '#64748b' }}>OpsxSQL · 安全合规的数据库管理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -346,7 +346,7 @@ export default function AnnouncementPage() {
|
||||
title={deleteConfirm?.msg || '确认操作'}
|
||||
variant="danger"
|
||||
confirmText="确定"
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); }}
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }}
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -268,7 +268,7 @@ export default function InstanceDetailPage() {
|
||||
title={deleteConfirm?.msg || '确认操作'}
|
||||
variant="danger"
|
||||
confirmText="确定"
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); }}
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }}
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -441,7 +441,7 @@ export default function InstanceGroupPage() {
|
||||
title={deleteConfirm?.msg || '确认操作'}
|
||||
variant="danger"
|
||||
confirmText="确定"
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); }}
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }}
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1453,7 +1453,7 @@ function CreateInstanceForm({ defaultType, onCancel, onCreated }: {
|
||||
title={deleteConfirm?.msg || '确认操作'}
|
||||
variant="danger"
|
||||
confirmText="确定"
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); }}
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }}
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -453,7 +453,7 @@ export default function NotificationPage() {
|
||||
title={deleteConfirm?.msg || '确认操作'}
|
||||
variant="danger"
|
||||
confirmText="确定"
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); }}
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }}
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -179,7 +179,7 @@ export default function NotificationsPage() {
|
||||
title={deleteConfirm?.msg || '确认操作'}
|
||||
variant="danger"
|
||||
confirmText="确定"
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); }}
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }}
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -313,7 +313,7 @@ export default function RedisDetailPage() {
|
||||
title={deleteConfirm?.msg || '确认操作'}
|
||||
variant="danger"
|
||||
confirmText="确定"
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); }}
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }}
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -530,7 +530,7 @@ export default function UsersPage() {
|
||||
title={deleteConfirm?.msg || '确认操作'}
|
||||
variant="danger"
|
||||
confirmText="确定"
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); }}
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }}
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -816,7 +816,7 @@ export default function WorkflowPage() {
|
||||
title={deleteConfirm?.msg || '确认操作'}
|
||||
variant="danger"
|
||||
confirmText="确定"
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); }}
|
||||
onConfirm={() => { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }}
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user