alphax/static/base.html
2026-05-13 22:32:50 +08:00

291 lines
16 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>{% block title %}AlphaX{% endblock %}</title>
<style>
/* ===== DESIGN.md Miro Tokens ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--yellow: #ffd02f; --yellow-deep: #fcb900; --yellow-light: #fff4c4; --yellow-dark: #746019;
--blue: #4262ff;
--green: #00b473; --green-light: rgba(0,180,115,.08);
--red: #e53e3e; --red-light: rgba(229,62,62,.08);
--primary: #1c1c1e; --on-primary: #ffffff; --canvas: #ffffff;
--surface: #f7f8fa;
--hairline: #e0e2e8; --hairline-soft: #eef0f3; --hairline-strong: #c7cad5;
--ink: #1c1c1e; --ink-deep: #050038;
--slate: #555a6a; --steel: #6b6f7e; --stone: #8e91a0; --muted: #a5a8b5;
--shadow: rgba(5,0,56,.08) 0px 12px 32px -4px;
--radius-sm:6px; --radius-md:8px; --radius-lg:12px; --radius-xl:16px; --radius-full:9999px;
--safe-bottom: env(safe-area-inset-bottom, 0px);
--sidebar-w: 220px;
--app-vh: 100vh;
}
{% block theme_override %}{% endblock %}
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(--surface);
line-height: 1.5; -webkit-font-smoothing: antialiased; text-size-adjust: 100%;
padding-bottom: var(--safe-bottom);
display: block;
}
a { color: inherit; text-decoration: none; }
/* ===== SIDEBAR ===== */
.sidebar {
position: fixed; left: 0; top: 0; width: var(--sidebar-w); height: var(--app-vh); max-height: var(--app-vh);
background: var(--canvas); border-right: 1px solid var(--hairline-soft);
display: flex; flex-direction: column; z-index: 100;
transition: transform .25s cubic-bezier(.4,0,.2,1);
}
.sidebar-brand {
display: flex; align-items: center; gap: 8px;
padding: 18px 20px; border-bottom: 1px solid var(--hairline-soft);
}
.brand-mark { width: 22px; height: 22px; background: var(--yellow); border-radius: 5px; display: grid; place-items: center; flex-shrink: 0; }
.brand-mark::after { content: ""; width: 7px; height: 7px; border: 1.5px solid var(--primary); border-radius: 50%; box-shadow: 5px -3px 0 -1.5px var(--primary); }
.brand-name { font-weight: 600; font-size: 14px; letter-spacing: -.2px; }
.beta-badge { display:inline-flex; align-items:center; height:19px; padding:0 7px; border-radius:var(--radius-full); background:var(--surface); border:1px solid var(--hairline); color:var(--steel); font-size:10px; font-weight:700; line-height:1; }
.sidebar-nav {
flex: 1 1 auto; min-height: 0; padding: 8px; display: flex; flex-direction: column; gap: 2px;
overflow-y: auto; -webkit-overflow-scrolling: touch;
}
.sidebar-link {
display: flex; align-items: center; gap: 10px;
padding: 10px 14px; border-radius: var(--radius-md);
font-size: 14px; font-weight: 500; color: var(--steel);
transition: .15s; cursor: pointer;
}
.sidebar-link:hover { color: var(--ink); background: var(--surface); }
.sidebar-link.active { color: var(--on-primary); background: var(--primary); font-weight: 600; }
.sidebar-link .link-icon { width: 18px; height: 18px; flex-shrink: 0; opacity: .6; }
.sidebar-link.active .link-icon { opacity: 1; }
.sidebar-user {
padding: 14px 16px calc(14px + var(--safe-bottom)); border-top: 1px solid var(--hairline-soft);
display: flex; align-items: center; gap: 8px; cursor: pointer;
font-size: 13px; color: var(--slate); transition: .15s; flex-shrink: 0;
}
.sidebar-user:hover { background: var(--surface); }
.user-avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--yellow); color: var(--primary); display: grid; place-items: center; font-weight: 700; font-size: 12px; flex-shrink: 0; }
.user-email { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-chevron { width: 12px; height: 12px; opacity: .4; flex-shrink: 0; }
/* User dropdown (attached to sidebar user) */
.user-dropdown {
display: none; position: absolute; left: 16px; bottom: 60px;
background: var(--canvas); border: 1px solid var(--hairline);
border-radius: var(--radius-lg); box-shadow: var(--shadow);
min-width: 188px; padding: 4px; z-index: 200;
}
.user-dropdown.show { display: block; }
.user-dropdown .dd-email { padding: 10px 14px; font-size: 12px; color: var(--stone); border-bottom: 1px solid var(--hairline-soft); margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dd-item { display: block; width: 100%; padding: 8px 14px; border: 0; background: transparent; font-size: 13px; color: var(--slate); text-align: left; cursor: pointer; border-radius: var(--radius-sm); transition: .1s; }
.dd-item:hover { background: var(--surface); color: var(--ink); }
.dd-item.danger { color: var(--red); }
.dd-item.danger:hover { background: var(--red-light); }
/* ===== MAIN CONTENT ===== */
.main-content { margin-left: var(--sidebar-w); min-width: 0; min-height: 100vh; overflow-y: auto; }
/* ===== MOBILE ===== */
.hamburger {
display: none; position: fixed; top: 12px; left: 12px; z-index: 110;
width: 36px; height: 36px; border-radius: var(--radius-md);
background: var(--canvas); border: 1px solid var(--hairline);
align-items: center; justify-content: center; cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,.06);
}
.hamburger span { display: block; width: 16px; height: 1.5px; background: var(--ink); position: relative; transition: .2s; }
.hamburger span::before, .hamburger span::after { content: ""; display: block; width: 16px; height: 1.5px; background: var(--ink); position: absolute; transition: .2s; }
.hamburger span::before { top: -5px; }
.hamburger span::after { top: 5px; }
.sidebar-overlay {
display: none; position: fixed; inset: 0; background: rgba(0,0,0,.35);
z-index: 99; transition: opacity .25s;
}
@media (max-width: 768px) {
.sidebar { transform: translateX(-100%); z-index: 100; height: var(--app-vh); max-height: var(--app-vh); }
.sidebar.open { transform: translateX(0); }
.sidebar-overlay.open { display: block; }
.hamburger { display: flex; }
.main-content { margin-left: 0; padding-top: 48px; }
}
/* ===== MODAL ===== */
.modal-overlay { display: none; position: fixed; inset: 0; z-index: 300; background: rgba(0,0,0,.35); align-items: center; justify-content: center; }
.modal-overlay.show { display: flex; }
.modal { background: var(--canvas); border-radius: var(--radius-xl); padding: 32px; width: 100%; max-width: 380px; box-shadow: var(--shadow); }
.modal h3 { font-size: 20px; font-weight: 600; margin-bottom: 20px; }
.modal .field { margin-bottom: 14px; }
.modal .field label { display: block; font-size: 13px; font-weight: 600; color: var(--slate); margin-bottom: 6px; }
.modal .field input { width: 100%; height: 42px; border: 1px solid var(--hairline-strong); border-radius: var(--radius-md); padding: 0 14px; font-size: 14px; outline: none; }
.modal .field input:focus { border-color: var(--blue); }
.modal-actions { display: flex; gap: 8px; margin-top: 20px; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 4px; border:0; cursor:pointer; font-weight:500; font-size:14px; line-height:1.3; transition: background .15s, transform .15s; border-radius: var(--radius-full); }
.btn:active { transform: scale(.98); }
.btn-primary { background: var(--primary); color: var(--on-primary); padding: 10px 20px; flex: 1; }
.btn-secondary { background: transparent; color: var(--ink); border: 1px solid var(--hairline-strong); padding: 10px 20px; flex: 1; }
.modal-msg { font-size: 13px; min-height: 20px; margin-top: 8px; }
.modal-msg.ok { color: var(--green); } .modal-msg.err { color: var(--red); }
</style>
{% block extra_head_css %}{% endblock %}
{% block extra_style %}{% endblock %}
</head>
<body>
{% if show_nav | default(True) %}
<svg style="display:none" aria-hidden="true">
<symbol id="svg-win" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="svg-lose" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18" stroke-linecap="round"/><line x1="6" y1="6" x2="18" y2="18" stroke-linecap="round"/></symbol>
<symbol id="svg-target" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></symbol>
<symbol id="svg-trendup" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18" stroke-linecap="round" stroke-linejoin="round"/><polyline points="17 6 23 6 23 12" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="svg-star" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></symbol>
<symbol id="svg-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol>
<symbol id="svg-spinner" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10" stroke-dasharray="31.4 31.4" stroke-linecap="round"/></symbol>
<symbol id="svg-dashboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></symbol>
<symbol id="svg-iterate" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></symbol>
<symbol id="svg-sentiment" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"/><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></symbol>
<symbol id="svg-subscribe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></symbol>
<symbol id="svg-admin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M5.3 20h13.4c1.1 0 2-.9 2-2 0-3.3-2.7-6-6-6H9.3c-3.3 0-6 2.7-6 6 0 1.1.9 2 2 2z"/></symbol>
<symbol id="svg-referral" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><polyline points="17 11 19 13 23 9"/></symbol>
<symbol id="svg-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></symbol>
</svg>
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<a class="sidebar-brand" href="/" aria-label="返回 AlphaX 首页">
<span class="brand-mark"></span>
<span class="brand-name">AlphaX</span>
<span class="beta-badge">Beta</span>
</a>
<nav class="sidebar-nav">
{% block nav_links %}
<a class="sidebar-link active" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/watchlist"><svg class="link-icon"><use href="#svg-star"/></svg>关注</a>
<a class="sidebar-link" href="/strategy"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link" href="/iteration"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
</nav>
<div class="sidebar-user" onclick="toggleUserMenu()">
<span class="user-avatar" id="userInitial">?</span>
<span class="user-email" id="userEmailShort">--</span>
<svg class="user-chevron"><use href="#svg-chevron-down"/></svg>
</div>
<div class="user-dropdown" id="userDropdown">
<div class="dd-email" id="ddEmail">--</div>
<button class="dd-item" onclick="showChangePwd()">修改密码</button>
<button class="dd-item danger" onclick="doLogout()">退出登录</button>
</div>
</aside>
<!-- Mobile overlay + hamburger -->
<div class="sidebar-overlay" id="sidebarOverlay" onclick="closeSidebar()"></div>
<button class="hamburger" id="hamburger" onclick="toggleSidebar()"><span></span></button>
{% endif %}
<div class="main-content">
{% block content %}{% endblock %}
</div>
{% if show_nav | default(True) %}
{% block password_modal %}
<div class="modal-overlay" id="pwdModal">
<div class="modal">
<h3>修改密码</h3>
<div class="field"><label>旧密码</label><input id="oldPwd" type="password" placeholder="输入当前密码"></div>
<div class="field"><label>新密码</label><input id="newPwd" type="password" placeholder="至少 8 位"></div>
<div class="field"><label>确认新密码</label><input id="cfmPwd" type="password" placeholder="再次输入新密码"></div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closePwdModal()">取消</button>
<button class="btn btn-primary" onclick="changePwd()">确认修改</button>
</div>
<div class="modal-msg" id="pwdMsg"></div>
</div>
</div>
{% endblock %}
<script>
// ====== AUTH (shared) ======
var API = '';
var currentUser = null;
var $ = function(id){ return document.getElementById(id); };
function setAppViewportHeight() {
document.documentElement.style.setProperty('--app-vh', (window.innerHeight || document.documentElement.clientHeight) + 'px');
}
setAppViewportHeight();
window.addEventListener('resize', setAppViewportHeight);
window.addEventListener('orientationchange', function(){ setTimeout(setAppViewportHeight, 250); });
async function loadUser() {
try {
var resp = await fetch(API + '/api/auth/me');
if (!resp.ok) return;
var data = await resp.json();
currentUser = data.user;
var email = currentUser.email || '--';
$('userInitial').textContent = email.charAt(0).toUpperCase();
$('userEmailShort').textContent = email.length > 16 ? email.slice(0,14) + '\u2026' : email;
$('ddEmail').textContent = email;
} catch(e) {}
}
function toggleUserMenu() { $('userDropdown').classList.toggle('show'); }
document.addEventListener('click', function(e) { if (!e.target.closest('.sidebar-user') && !e.target.closest('.user-dropdown')) $('userDropdown').classList.remove('show'); });
function showChangePwd() { $('userDropdown').classList.remove('show'); $('pwdModal').classList.add('show'); }
function closePwdModal() { $('pwdModal').classList.remove('show'); $('pwdMsg').textContent=''; $('pwdMsg').className='modal-msg'; }
async function changePwd() {
var o=$('oldPwd').value,n=$('newPwd').value,c=$('cfmPwd').value;
if(n!==c){ $('pwdMsg').textContent='两次密码不一致'; $('pwdMsg').className='modal-msg err'; return; }
try{
var r=await fetch(API+'/api/auth/change-password',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({old_password:o,new_password:n})});
var d=await r.json();
if(r.ok){ $('pwdMsg').textContent='密码已修改'; $('pwdMsg').className='modal-msg ok'; setTimeout(closePwdModal,1500); }
else { $('pwdMsg').textContent=d.detail||'修改失败'; $('pwdMsg').className='modal-msg err'; }
}catch(e){ $('pwdMsg').textContent='网络错误'; $('pwdMsg').className='modal-msg err'; }
}
async function doLogout() {
try{ await fetch(API+'/api/auth/logout',{method:'POST'}); }catch(e){}
window.location.href='/auth';
}
// Mobile sidebar
function toggleSidebar() {
$('sidebar').classList.toggle('open');
$('sidebarOverlay').classList.toggle('open');
}
function closeSidebar() {
$('sidebar').classList.remove('open');
$('sidebarOverlay').classList.remove('open');
}
// Admin check
fetch(API+'/api/admin/check').then(function(r){return r.json()}).then(function(d){
if(d&&d.is_admin){
var links=document.querySelectorAll('.admin-link');
for(var i=0;i<links.length;i++)links[i].style.display='';
}
}).catch(function(){});
loadUser();
</script>
{% endif %}
{% block extra_script %}{% endblock %}
</body>
</html>