alphax/static/auth.html
2026-05-18 12:44:53 +08:00

327 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>登录 / 注册 — AlphaX Agent</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--yellow: #ffd02f; --yellow-light: #fff4c4; --yellow-dark: #746019;
--blue: #4262ff; --green: #00b473; --red: #e53e3e;
--primary: #1c1c1e; --on-primary: #ffffff; --canvas: #ffffff;
--surface: #f7f8fa; --surface-soft: #fafbfc;
--hairline: #e0e2e8; --hairline-soft: #eef0f3; --hairline-strong: #c7cad5;
--ink: #1c1c1e; --ink-deep: #050038;
--charcoal: #2c2c34; --slate: #555a6a; --steel: #6b6f7e; --stone: #8e91a0; --muted: #a5a8b5;
--radius-xs:4px; --radius-sm:6px; --radius-md:8px; --radius-lg:12px; --radius-xl:16px; --radius-full:9999px;
--safe-bottom: env(safe-area-inset-bottom, 0px);
}
html { overflow-x: hidden; }
body {
min-height: 100vh; overflow-x: hidden;
font-family: 'Noto Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--ink); background: var(--canvas);
line-height: 1.5; -webkit-font-smoothing: antialiased; text-size-adjust: 100%;
display: flex; align-items: center; justify-content: center;
padding-bottom: var(--safe-bottom);
}
a { color: inherit; text-decoration: none; }
.page { width: 100%; max-width: 420px; padding: 48px 32px; }
/* Brand — DESIGN.md top-nav pattern: yellow mark + wordmark */
.brand { display: flex; align-items: center; gap: 8px; margin-bottom: 40px; }
.brand-mark { width: 24px; height: 24px; background: var(--yellow); border-radius: 6px; display: grid; place-items: center; }
.brand-mark::after { content: ""; width: 8px; height: 8px; border: 1.5px solid var(--primary); border-radius: 50%; box-shadow: 6px -4px 0 -2px var(--primary); }
.brand-name { font-weight: 500; font-size: 16px; letter-spacing: 0; white-space: nowrap; }
/* Tab pills — DESIGN.md pill-tab pattern */
.tabs { display: grid; grid-template-columns: 1fr 1fr; padding: 4px; background: var(--surface); border-radius: var(--radius-full); margin-bottom: 32px; }
.tab { border: 0; background: transparent; color: var(--steel); padding: 10px 16px; border-radius: var(--radius-full); font-weight: 500; font-size: 14px; cursor: pointer; transition: .15s; line-height: 1.3; }
.tab.active { background: var(--primary); color: var(--on-primary); }
.form { display: none; flex-direction: column; gap: 16px; animation: rise .2s ease; }
.form.active { display: flex; }
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
/* DESIGN.md text-input pattern */
.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font-size: 13px; font-weight: 600; color: var(--slate); }
.input-wrap { position: relative; width: 100%; }
.input-wrap input, .field input {
width: 100%; height: 44px; border: 1px solid var(--hairline-strong); border-radius: var(--radius-md);
padding: 0 14px; font-size: 16px; color: var(--ink); background: var(--canvas);
outline: none; transition: border .15s; -webkit-appearance: none;
}
.input-wrap input:focus, .field input:focus { border: 2px solid var(--blue); }
.input-wrap input::placeholder, .field input::placeholder { color: var(--muted); }
/* Email + send-code row */
.field-row { display: flex; gap: 8px; align-items: flex-end; }
.field-row .field { flex: 1; }
.btn-send {
flex-shrink: 0; height: 44px; padding: 0 20px;
background: var(--primary); color: var(--on-primary);
border: 0; border-radius: var(--radius-full);
font-weight: 500; font-size: 14px; cursor: pointer;
transition: background .15s, transform .15s;
white-space: nowrap; -webkit-tap-highlight-color: transparent;
line-height: 1.3;
}
.btn-send:active { background: var(--charcoal); transform: scale(.97); }
.btn-send:disabled { background: var(--hairline); color: var(--muted); cursor: not-allowed; }
/* Password toggle */
.pwd-toggle {
position: absolute; right: 0; top: 0; height: 44px; width: 44px;
display: flex; align-items: center; justify-content: center;
border: 0; background: transparent; cursor: pointer; color: var(--stone); padding: 0;
transition: color .15s; -webkit-tap-highlight-color: transparent;
}
.pwd-toggle:active { color: var(--slate); }
.pwd-toggle svg { width: 20px; height: 20px; }
/* DESIGN.md button-primary pattern */
.btn-primary {
display: flex; align-items: center; justify-content: center;
width: 100%; height: 44px;
background: var(--primary); color: var(--on-primary);
border: 0; border-radius: var(--radius-full);
font-weight: 500; font-size: 14px; cursor: pointer;
transition: background .15s, transform .15s;
-webkit-tap-highlight-color: transparent; margin-top: 4px;
}
.btn-primary:active { background: var(--charcoal); transform: scale(.97); }
/* DESIGN.md badge-tag-yellow for notice */
.notice {
background: var(--yellow-light); color: var(--yellow-dark);
border-radius: var(--radius-md); padding: 10px 14px;
font-size: 13px; line-height: 1.5;
}
.resend-hint { font-size: 12px; color: var(--stone); margin-top: -10px; }
.resend-hint a { color: var(--blue); font-weight: 500; cursor: pointer; }
.code-sent-badge {
display: flex; align-items: center; gap: 6px;
font-size: 13px; color: var(--green); font-weight: 600; margin-top: -8px;
}
.code-sent-badge svg { width: 16px; height: 16px; }
.msg { font-size: 13px; line-height: 1.5; min-height: 20px; }
.msg.ok { color: var(--green); } .msg.err { color: var(--red); } .msg.warn { color: var(--yellow-dark); }
/* DESIGN.md button-link for back */
.back-link { display: block; text-align: center; margin-top: 36px; padding-bottom: 24px; font-size: 13px; color: var(--stone); transition: color .15s; }
.back-link:hover { color: var(--slate); }
/* ====== MOBILE ====== */
@media (max-width: 480px) {
.page { padding: 32px 20px 48px; }
.brand { margin-bottom: 32px; }
.tabs { margin-bottom: 28px; }
.field-row .field { flex: 1; min-width: 0; }
.btn-send { flex-shrink: 0; padding: 0 16px; font-size: 13px; }
.back-link { margin-top: 32px; padding-bottom: calc(24px + var(--safe-bottom)); }
}
@media (max-width: 360px) {
.page { padding: 24px 16px 40px; }
.brand { margin-bottom: 24px; }
.tabs { margin-bottom: 24px; }
.tab { padding: 8px 12px; font-size: 13px; }
.input-wrap input, .field input { height: 44px; font-size: 15px; }
.btn-send { height: 44px; font-size: 13px; }
.btn-primary { height: 44px; font-size: 15px; }
.pwd-toggle { height: 44px; width: 44px; }
.form { gap: 14px; }
}
</style>
</head>
<body>
<div class="page">
<a class="brand" href="/">
<span class="brand-mark"></span>
<span class="brand-name">AlphaX Agent</span>
</a>
<div class="tabs">
<button class="tab active" id="tabRegister" onclick="setTab('register')">创建账号</button>
<button class="tab" id="tabLogin" onclick="setTab('login')">会员登录</button>
</div>
<!-- ===== 注册表单 ===== -->
<div id="registerForm" class="form active">
<div class="field">
<label>邮箱</label>
<input id="regEmail" type="email" placeholder="you@example.com" autocomplete="email" inputmode="email">
</div>
<div class="field-row">
<div class="field">
<label>邮箱验证码</label>
<input id="verifyCode" type="text" inputmode="numeric" autocomplete="one-time-code" placeholder="输入 6 位验证码" maxlength="6">
</div>
<button class="btn-send" id="sendCodeBtn" onclick="sendCode()">发送验证码</button>
</div>
<div id="codeSentBadge" class="code-sent-badge" style="display:none">
<svg viewBox="0 0 16 16" fill="none"><path d="M3 8l3.5 3.5L13 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
验证码已发送至邮箱
</div>
<div class="resend-hint" id="resendHint" style="display:none">
没收到?<a onclick="resendCode()">重新发送</a>
</div>
<div class="field">
<label>设置密码</label>
<div class="input-wrap">
<input id="regPassword" type="password" placeholder="至少 8 位,含字母和数字" autocomplete="new-password">
<button class="pwd-toggle" id="regPwdToggle" onclick="togglePwd('regPassword', 'regPwdToggle')" type="button" aria-label="显示密码">
<svg id="regPwdEye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</div>
<div class="field">
<label>邀请码</label>
<input id="regInvite" type="text" placeholder="请输入邀请码" autocomplete="off" required>
</div>
<button class="btn-primary" onclick="doRegister()">注册</button>
<div id="regMsg" class="msg"></div>
</div>
<!-- ===== 登录表单 ===== -->
<div id="loginForm" class="form">
<div class="field">
<label>邮箱</label>
<input id="loginEmail" type="email" placeholder="you@example.com" autocomplete="email" inputmode="email">
</div>
<div class="field">
<label>密码</label>
<div class="input-wrap">
<input id="loginPassword" type="password" placeholder="输入密码" autocomplete="current-password">
<button class="pwd-toggle" id="loginPwdToggle" onclick="togglePwd('loginPassword', 'loginPwdToggle')" type="button" aria-label="显示密码">
<svg id="loginPwdEye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</div>
<button class="btn-primary" id="loginSubmitBtn" onclick="loginUser()">登录</button>
<div id="loginMsg" class="msg"></div>
</div>
</div>
<script>
function $(id){ return document.getElementById(id); }
function setMsg(id, text, cls){ var el=$(id); el.className='msg '+(cls||''); el.textContent=text||''; }
function setTab(tab){
document.querySelectorAll('.tab').forEach(function(b,i){ b.classList.toggle('active', (tab==='register'&&i===0)||(tab==='login'&&i===1)); });
$('registerForm').classList.toggle('active', tab==='register');
$('loginForm').classList.toggle('active', tab==='login');
}
async function post(url, body){
var r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body||{})});
var data=await r.json().catch(function(){ return {detail:'请求失败'}; });
if(!r.ok) throw new Error(data.detail||'请求失败');
return data;
}
function togglePwd(inputId, toggleId) {
var inp = $(inputId);
var eye = $(inputId === 'regPassword' ? 'regPwdEye' : 'loginPwdEye');
var isPass = inp.type === 'password';
inp.type = isPass ? 'text' : 'password';
if (isPass) {
eye.innerHTML = '<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>';
} else {
eye.innerHTML = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>';
}
}
var codeSent = false;
(function(){
var m = location.search.match(/[?&]invite=([^&]+)/);
if(m){ $('regInvite').value = decodeURIComponent(m[1]); }
})();
async function sendCode(){
try{
var email = $('regEmail').value;
if(!email){ setMsg('regMsg','请先输入邮箱','err'); return; }
var data = await post('/api/auth/send-code',{email:email});
if(data.dev_verification_code) $('verifyCode').value = data.dev_verification_code;
$('codeSentBadge').style.display = 'flex';
$('resendHint').style.display = 'block';
codeSent = true;
var devMsg = data.dev_verification_code ? ' 开发环境验证码:'+data.dev_verification_code : '';
setMsg('regMsg', '验证码已发送至 '+email+devMsg,'ok');
var btn = $('sendCodeBtn'), sec = 60;
btn.disabled = true; btn.textContent = sec+'s';
var timer = setInterval(function(){
sec--; btn.textContent = sec+'s';
if(sec<=0){ clearInterval(timer); btn.disabled = false; btn.textContent = '发送验证码'; }
}, 1000);
}catch(e){ setMsg('regMsg', e.message, 'err'); }
}
async function doRegister(){
try{
if(!codeSent){ setMsg('regMsg','请先点击「发送验证码」获取邮箱验证码','err'); return; }
var pwd = $('regPassword').value;
if(!pwd || pwd.length < 8){ setMsg('regMsg','密码至少 8 位','err'); return; }
var code = $('verifyCode').value;
if(!code){ setMsg('regMsg','请输入验证码','err'); return; }
var invite = $('regInvite').value.trim();
if(!invite){ setMsg('regMsg','请输入邀请码','err'); return; }
var data = await post('/api/auth/complete-registration',{
email: $('regEmail').value,
code: code,
password: pwd,
invite_code: invite
});
setMsg('regMsg', data.message||'注册成功,正在跳转……','ok');
$('loginEmail').value = $('regEmail').value;
setTimeout(function(){
setTab('login');
setMsg('loginMsg','注册成功,请登录','ok');
}, 600);
}catch(e){ setMsg('regMsg', e.message, 'err'); }
}
async function resendCode(){
try{
var data = await post('/api/auth/resend-verification',{email:$('regEmail').value});
if(data.dev_verification_code) $('verifyCode').value = data.dev_verification_code;
setMsg('regMsg', data.dev_verification_code ? '验证码:'+data.dev_verification_code : '验证码已重新发送','ok');
}catch(e){ setMsg('regMsg', e.message, 'err'); }
}
var loginSubmitting = false;
async function loginUser(){
if(loginSubmitting) return;
loginSubmitting = true;
var btn = $('loginSubmitBtn');
if(btn) btn.disabled = true;
try{
var data = await post('/api/auth/login',{email:$('loginEmail').value,password:$('loginPassword').value});
var next = data.next || (data.subscription_active ? '/app' : '/subscription?welcome=1');
setMsg('loginMsg', data.subscription_active ? '登录成功,正在进入机会总览…' : '登录成功,先开通免费体验套餐…','ok');
setTimeout(function(){ window.location.href = next; }, 500);
}catch(e){
setMsg('loginMsg', e.message, 'err');
loginSubmitting = false;
if(btn) btn.disabled = false;
}
}
(function(){
if (location.search.indexOf('tab=login') !== -1) setTab('login');
['loginEmail','loginPassword'].forEach(function(id){
var input = $(id);
if(!input) return;
input.addEventListener('keydown', function(e){
if(e.key === 'Enter'){
e.preventDefault();
loginUser();
}
});
});
})();
</script>
</body>
</html>