| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>PlateRecApp - 登录</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: #0f1419;
- color: #e1e8ed;
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .login-container {
- width: 100%;
- max-width: 400px;
- padding: 20px;
- }
- .login-card {
- background: #1a2332;
- border-radius: 12px;
- padding: 40px 32px;
- box-shadow: 0 8px 32px rgba(0,0,0,0.4);
- border: 1px solid #2a3a4a;
- }
- .logo {
- text-align: center;
- margin-bottom: 32px;
- }
- .logo h1 {
- font-size: 24px;
- font-weight: 600;
- color: #4fc3f7;
- margin-bottom: 4px;
- }
- .logo p {
- font-size: 13px;
- color: #6b7b8d;
- }
- .form-group {
- margin-bottom: 20px;
- }
- .form-group label {
- display: block;
- font-size: 13px;
- color: #8899aa;
- margin-bottom: 6px;
- font-weight: 500;
- }
- .form-group input {
- width: 100%;
- padding: 12px 16px;
- background: #0f1419;
- border: 1px solid #2a3a4a;
- border-radius: 8px;
- color: #e1e8ed;
- font-size: 15px;
- transition: border-color 0.2s, box-shadow 0.2s;
- outline: none;
- }
- .form-group input:focus {
- border-color: #4fc3f7;
- box-shadow: 0 0 0 3px rgba(79,195,247,0.15);
- }
- .form-group input::placeholder {
- color: #4a5568;
- }
- .form-options {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24px;
- font-size: 13px;
- }
- .remember-me {
- display: flex;
- align-items: center;
- gap: 6px;
- cursor: pointer;
- color: #8899aa;
- }
- .remember-me input[type="checkbox"] {
- width: 16px;
- height: 16px;
- accent-color: #4fc3f7;
- cursor: pointer;
- }
- .btn-login {
- width: 100%;
- padding: 12px;
- background: linear-gradient(135deg, #4fc3f7, #0288d1);
- border: none;
- border-radius: 8px;
- color: white;
- font-size: 15px;
- font-weight: 600;
- cursor: pointer;
- transition: opacity 0.2s, transform 0.1s;
- }
- .btn-login:hover { opacity: 0.9; }
- .btn-login:active { transform: scale(0.98); }
- .btn-login:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
- .error-msg {
- background: rgba(239,68,68,0.1);
- border: 1px solid rgba(239,68,68,0.3);
- color: #f87171;
- padding: 10px 14px;
- border-radius: 8px;
- font-size: 13px;
- margin-bottom: 16px;
- display: none;
- }
- .error-msg.show { display: block; }
- .rate-limit-msg {
- background: rgba(245,158,11,0.1);
- border: 1px solid rgba(245,158,11,0.3);
- color: #fbbf24;
- padding: 10px 14px;
- border-radius: 8px;
- font-size: 13px;
- margin-bottom: 16px;
- display: none;
- }
- .footer {
- text-align: center;
- margin-top: 24px;
- font-size: 12px;
- color: #4a5568;
- }
- .loading-spinner {
- display: inline-block;
- width: 16px;
- height: 16px;
- border: 2px solid rgba(255,255,255,0.3);
- border-top-color: white;
- border-radius: 50%;
- animation: spin 0.6s linear infinite;
- margin-right: 8px;
- vertical-align: middle;
- }
- @keyframes spin { to { transform: rotate(360deg); } }
-
- @media (max-width: 480px) {
- .login-container { padding: 12px; }
- .login-card { padding: 24px 20px; }
- .logo h1 { font-size: 20px; }
- .form-group input { padding: 10px 12px; font-size: 14px; }
- .btn-login { padding: 10px; font-size: 14px; }
- }
- </style>
- </head>
- <body>
- <div class="login-container">
- <div class="login-card">
- <div class="logo">
- <h1>🔒 PlateRecApp</h1>
- <p>车牌识别管理系统</p>
- </div>
- <div id="error-msg" class="error-msg"></div>
- <div id="rate-limit-msg" class="rate-limit-msg"></div>
- <form id="login-form" autocomplete="off">
- <div class="form-group">
- <label for="username">用户名</label>
- <input type="text" id="username" name="username"
- placeholder="请输入用户名" required autofocus
- autocomplete="username">
- </div>
- <div class="form-group">
- <label for="password">密码</label>
- <input type="password" id="password" name="password"
- placeholder="请输入密码" required
- autocomplete="current-password">
- </div>
- <div class="form-options">
- <label class="remember-me">
- <input type="checkbox" id="remember" name="remember">
- 记住登录 (7天)
- </label>
- </div>
- <button type="submit" class="btn-login" id="btn-login">登 录</button>
- </form>
- <div class="footer">
- PlateRecApp v5.0 | Secure Login
- </div>
- </div>
- </div>
- <script>
- const form = document.getElementById('login-form');
- const btnLogin = document.getElementById('btn-login');
- const errorDiv = document.getElementById('error-msg');
- const rateLimitDiv = document.getElementById('rate-limit-msg');
- // Check if already logged in
- fetch('/api/auth/status')
- .then(r => r.json())
- .then(data => {
- if (data.authenticated) {
- const redirect = new URLSearchParams(window.location.search).get('redirect') || '/';
- window.location.href = redirect;
- }
- })
- .catch(() => {});
- form.addEventListener('submit', async (e) => {
- e.preventDefault();
- errorDiv.classList.remove('show');
- rateLimitDiv.classList.remove('show');
- btnLogin.disabled = true;
- btnLogin.innerHTML = '<span class="loading-spinner"></span>登录中...';
- const username = document.getElementById('username').value.trim();
- const password = document.getElementById('password').value;
- const remember = document.getElementById('remember').checked;
- try {
- const resp = await fetch('/api/auth/login', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ username, password, remember })
- });
- const data = await resp.json();
- if (resp.status === 429) {
- rateLimitDiv.textContent = data.message || '请求过于频繁,请稍后再试';
- rateLimitDiv.classList.add('show');
- } else if (resp.status === 401) {
- errorDiv.textContent = data.message || '用户名或密码错误';
- errorDiv.classList.add('show');
- document.getElementById('password').value = '';
- document.getElementById('password').focus();
- } else if (resp.status === 423) {
- errorDiv.textContent = data.message || '账户已锁定,请稍后再试';
- errorDiv.classList.add('show');
- } else if (data.success) {
- const redirect = new URLSearchParams(window.location.search).get('redirect') || '/';
- window.location.href = redirect;
- return;
- } else {
- errorDiv.textContent = data.message || '登录失败';
- errorDiv.classList.add('show');
- }
- } catch (err) {
- errorDiv.textContent = '网络错误,请重试';
- errorDiv.classList.add('show');
- }
- btnLogin.disabled = false;
- btnLogin.textContent = '登 录';
- });
- </script>
- </body>
- </html>
|