alphax/static/auth.html
2026-05-14 11:21:21 +08:00

314 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 Crypto</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 Crypto</span>
</a>
<div class="notice" style="margin-bottom:16px">
<strong>AlphaX Agent Crypto</strong><br>
提前发现机会,别在强信号后追高。登录或开启免费体验,创建账号后可前往订阅中心。
</div>
<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" onclick="loginUser()">登录</button>
<div id="loginMsg" class="msg"></div>
</div>
<a class="back-link" href="/subscription">前往订阅中心</a>
</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'); }
}
async function loginUser(){
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'); }
}
(function(){
if (location.search.indexOf('tab=login') !== -1) setTab('login');
})();
</script>
</body>
</html>