| <!DOCTYPE html> |
| <html lang="en-US"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>QA Annotation System - Login</title> |
| <link rel="stylesheet" href="/static/css/auth.css"> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="auth-container"> |
| <div class="auth-header"> |
| <button class="auth-lang-switch" id="languageSwitchBtn" type="button" title="Switch Language"> |
| <span id="currentLanguage">English</span> |
| </button> |
| <h1 data-i18n="auth.loginTitle">QA Annotation System</h1> |
| <div class="tab-buttons"> |
| <button class="tab-btn active" data-tab="login" data-i18n="auth.login">Login</button> |
| <button class="tab-btn" data-tab="register" data-i18n="auth.register">Register</button> |
| </div> |
| </div> |
|
|
| |
| <div class="form-container active" id="login-form"> |
| <form id="loginForm"> |
| <div class="form-group"> |
| <label for="login-username" data-i18n="auth.username">Username</label> |
| <input |
| type="text" |
| id="login-username" |
| name="username" |
| required |
| minlength="3" |
| maxlength="50" |
| value="admin" |
| data-i18n-placeholder="auth.usernamePlaceholder" |
| placeholder="Please enter username" |
| > |
| </div> |
| <div class="form-group"> |
| <label for="login-password" data-i18n="auth.password">Password</label> |
| <div class="password-field"> |
| <input |
| type="password" |
| id="login-password" |
| name="password" |
| required |
| minlength="6" |
| value="123456" |
| data-i18n-placeholder="auth.passwordPlaceholder" |
| placeholder="Please enter password" |
| > |
| <button |
| type="button" |
| class="password-toggle" |
| id="login-password-toggle" |
| data-i18n-aria-label="auth.showPassword" |
| aria-label="Show password" |
| title="Show password" |
| > |
| <svg class="icon-eye" viewBox="0 0 24 24" aria-hidden="true"> |
| <path d="M12 5C7 5 2.73 8.11 1 12c1.73 3.89 6 7 11 7s9.27-3.11 11-7c-1.73-3.89-6-7-11-7zm0 11a4 4 0 1 1 0-8 4 4 0 0 1 0 8z" fill="currentColor"/> |
| </svg> |
| <svg class="icon-eye-off" viewBox="0 0 24 24" aria-hidden="true"> |
| <path d="M12 7a4.5 4.5 0 0 1 4.5 4.5c0 .57-.11 1.12-.31 1.62l2.2 2.2A11.77 11.77 0 0 0 23 12c-1.73-3.89-6-7-11-7-1.45 0-2.84.24-4.12.67l1.97 1.97c.5-.2 1.05-.31 1.62-.31zM2 4.27l2.28 2.28A11.74 11.74 0 0 0 1 12c1.73 3.89 6 7 11 7 1.55 0 3.03-.3 4.38-.84l2.58 2.58L21.73 22 20 20.27 3.27 3 2 4.27zm7.53 7.53-1.77-1.77A2.5 2.5 0 0 0 9.5 12c0 1.38 1.12 2.5 2.5 2.5.47 0 .91-.13 1.29-.35l-1.76-1.76z" fill="currentColor"/> |
| </svg> |
| </button> |
| </div> |
| </div> |
| <div class="form-group"> |
| <button type="submit" class="btn btn-primary"><span data-i18n="auth.login">Login</span></button> |
| <p class="demo-hint" data-i18n="auth.demoHint">Demo: Default admin credentials are pre-filled. Click Login to get started.</p> |
| </div> |
| <div class="message" id="login-message"></div> |
| </form> |
| </div> |
|
|
| |
| <div class="form-container" id="register-form"> |
| <form id="registerForm"> |
| <div class="form-group"> |
| <label for="register-username" data-i18n="auth.username">Username</label> |
| <input |
| type="text" |
| id="register-username" |
| name="username" |
| required |
| minlength="3" |
| maxlength="50" |
| data-i18n-placeholder="auth.usernamePlaceholderWithHint" |
| placeholder="Please enter username (3-50 characters)" |
| > |
| </div> |
| <div class="form-group"> |
| <label for="register-password" data-i18n="auth.password">Password</label> |
| <input |
| type="password" |
| id="register-password" |
| name="password" |
| required |
| minlength="6" |
| data-i18n-placeholder="auth.passwordPlaceholderWithHint" |
| placeholder="Please enter password (at least 6 characters)" |
| > |
| </div> |
| <div class="form-group"> |
| <label for="register-fullname" data-i18n="auth.fullnameOptional">Full Name (optional)</label> |
| <input |
| type="text" |
| id="register-fullname" |
| name="full_name" |
| maxlength="100" |
| data-i18n-placeholder="auth.fullnamePlaceholder" |
| placeholder="Please enter full name" |
| > |
| </div> |
| <div class="form-group"> |
| <label for="register-organization" data-i18n="auth.organizationOptional">Organization (optional)</label> |
| <select id="register-organization" name="organization"> |
| <option value="" data-i18n="auth.selectOrganization">Please select</option> |
| <option value="崖州湾实验室">崖州湾实验室</option> |
| <option value="之江实验室">之江实验室</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label for="register-team" data-i18n="auth.teamOptional">Team (optional)</label> |
| <input |
| type="text" |
| id="register-team" |
| name="team" |
| maxlength="100" |
| data-i18n-placeholder="auth.teamPlaceholder" |
| placeholder="Please enter team" |
| > |
| </div> |
| <div class="form-group"> |
| <label for="register-species" data-i18n="auth.speciesOptional">Species (optional)</label> |
| <input |
| type="text" |
| id="register-species" |
| name="species" |
| maxlength="100" |
| data-i18n-placeholder="auth.speciesPlaceholder" |
| placeholder="Please enter species" |
| > |
| </div> |
| <div class="form-group"> |
| <button type="submit" class="btn btn-primary"><span data-i18n="auth.register">Register</span></button> |
| </div> |
| <div class="message" id="register-message"></div> |
| </form> |
| </div> |
| </div> |
| </div> |
| <script src="https://unpkg.com/i18next@23.7.11/dist/umd/i18next.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.js"></script> |
| <script src="/static/js/api.js"></script> |
| <script src="/static/js/i18n-helper.js"></script> |
| <script src="/static/js/auth.js"></script> |
|
|
| |
| <script> |
| |
| window.addEventListener('load', async function() { |
| if (typeof i18next === 'undefined') { |
| console.error('i18next 库未加载'); |
| return; |
| } |
| |
| const savedLanguage = localStorage.getItem('appLanguage') || 'en-US'; |
| |
| try { |
| const [zhCN, enUS] = await Promise.all([ |
| fetch('/static/locales/zh-CN.json').then(r => r.json()), |
| fetch('/static/locales/en-US.json').then(r => r.json()) |
| ]); |
| |
| await i18next.init({ |
| lng: savedLanguage, |
| fallbackLng: 'en-US', |
| debug: false, |
| resources: { |
| 'zh-CN': { |
| translation: zhCN.translation |
| }, |
| 'en-US': { |
| translation: enUS.translation |
| } |
| } |
| }); |
| |
| window.i18next = i18next; |
| window.t = function(key, options) { |
| return i18next.t(key, options); |
| }; |
| |
| updatePageLanguage(); |
| |
| const langBtn = document.getElementById('languageSwitchBtn'); |
| if (langBtn) { |
| langBtn.addEventListener('click', toggleLanguage); |
| } |
| } catch (error) { |
| console.error('i18next 初始化失败:', error); |
| } |
| }); |
| |
| function toggleLanguage() { |
| if (!window.i18next) return; |
| |
| const currentLang = window.i18next.language; |
| const newLang = currentLang === 'zh-CN' ? 'en-US' : 'zh-CN'; |
| |
| window.i18next.changeLanguage(newLang, () => { |
| localStorage.setItem('appLanguage', newLang); |
| updatePageLanguage(); |
| }); |
| } |
| |
| function updatePageLanguage() { |
| if (!window.i18next) return; |
| |
| const lang = window.i18next.language; |
| document.documentElement.lang = lang; |
| |
| const langBtn = document.getElementById('currentLanguage'); |
| if (langBtn) { |
| langBtn.textContent = lang === 'zh-CN' ? '中文' : 'English'; |
| } |
| |
| |
| document.querySelectorAll('[data-i18n]').forEach(element => { |
| const key = element.getAttribute('data-i18n'); |
| const translation = window.i18next.t(key); |
| |
| if (element.children.length === 1 && element.children[0].tagName === 'SPAN') { |
| const span = element.children[0]; |
| if (span.hasAttribute('data-i18n')) { |
| span.textContent = window.i18next.t(span.getAttribute('data-i18n')); |
| } else { |
| element.textContent = translation; |
| } |
| } else { |
| element.textContent = translation; |
| } |
| }); |
| |
| |
| document.querySelectorAll('[data-i18n-placeholder]').forEach(element => { |
| const key = element.getAttribute('data-i18n-placeholder'); |
| element.placeholder = window.i18next.t(key); |
| }); |
| |
| |
| document.querySelectorAll('[data-i18n-aria-label]').forEach(element => { |
| const key = element.getAttribute('data-i18n-aria-label'); |
| element.setAttribute('aria-label', window.i18next.t(key)); |
| element.setAttribute('title', window.i18next.t(key)); |
| }); |
| |
| const loginPassword = document.getElementById('login-password'); |
| const loginToggle = document.getElementById('login-password-toggle'); |
| if (loginPassword && loginToggle && loginPassword.type === 'text') { |
| const label = window.i18next.t('auth.hidePassword'); |
| loginToggle.setAttribute('aria-label', label); |
| loginToggle.setAttribute('title', label); |
| } |
| |
| |
| document.title = window.i18next.t('auth.loginTitle') + ' - ' + window.i18next.t('auth.login'); |
| } |
| </script> |
| </body> |
| </html> |
|
|