309 lines
14 KiB
HTML
309 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</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: -.2px; }
|
|
|
|
/* 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</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" onclick="loginUser()">登录</button>
|
|
<div id="loginMsg" class="msg"></div>
|
|
</div>
|
|
|
|
<a class="back-link" href="/">← 返回首页</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>
|