alphax/static/base.html
2026-05-16 23:12:45 +08:00

315 lines
20 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>{% block title %}AlphaX Agent Crypto{% 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 14px; 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: 13px; letter-spacing: 0; min-width: 0; white-space: nowrap; }
.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-section-label { padding: 12px 14px 5px; color: var(--muted); font-size: 10px; font-weight: 900; letter-spacing: .08em; }
.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-pipeline" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="5" cy="6" r="2"/><circle cx="19" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M7 6h10"/><path d="M6.5 7.7 11 16"/><path d="M17.5 7.7 13 16"/></symbol>
<symbol id="svg-cron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/><path d="M4 4l3 3"/><path d="M20 4l-3 3"/></symbol>
<symbol id="svg-ai" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="5" width="14" height="14" rx="3"/><path d="M9 1v4"/><path d="M15 1v4"/><path d="M9 19v4"/><path d="M15 19v4"/><path d="M1 9h4"/><path d="M1 15h4"/><path d="M19 9h4"/><path d="M19 15h4"/><path d="M9 15l1.5-6h3L15 15"/><path d="M10 13h4"/></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-onchain" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="6" cy="7" r="3"/><circle cx="18" cy="7" r="3"/><circle cx="12" cy="18" r="3"/><path d="M8.6 8.8 10.7 15"/><path d="M15.4 8.8 13.3 15"/><path d="M9 7h6"/></symbol>
<symbol id="svg-paper" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19V5"/><path d="M4 19h16"/><path d="M7 15l3-4 3 2 5-7"/><path d="M17 6h1v1"/></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-config" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h10"/><path d="M4 17h16"/><circle cx="17" cy="7" r="3"/><circle cx="7" cy="17" r="3"/></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 Agent Crypto 首页">
<span class="brand-mark"></span>
<span class="brand-name">AlphaX Agent Crypto</span>
</a>
<nav class="sidebar-nav">
<div class="sidebar-section-label">交易</div>
<a class="sidebar-link {% if active_nav | default('app') == 'app' %}active{% endif %}" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link {% if active_nav == 'paper_trading' %}active{% endif %}" href="/paper-trading"><svg class="link-icon"><use href="#svg-paper"/></svg>模拟交易</a>
<div class="sidebar-section-label">研究</div>
<a class="sidebar-link {% if active_nav == 'market' %}active{% endif %}" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link {% if active_nav == 'sentiment' %}active{% endif %}" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>实时舆情</a>
<a class="sidebar-link {% if active_nav == 'onchain' %}active{% endif %}" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<div class="sidebar-section-label">账户</div>
<a class="sidebar-link {% if active_nav == 'subscription' %}active{% endif %}" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link {% if active_nav == 'referral' %}active{% endif %}" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>邀请</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link {% if active_nav == 'pipeline' %}active{% endif %}" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link {% if active_nav == 'llm_insights' %}active{% endif %}" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link {% if active_nav == 'strategy' %}active{% endif %}" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link {% if active_nav == 'iteration' %}active{% endif %}" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<div class="sidebar-section-label admin-link" style="display:none">系统</div>
<a class="sidebar-link admin-link {% if active_nav == 'config' %}active{% endif %}" href="/config" style="display:none"><svg class="link-icon"><use href="#svg-config"/></svg>配置中心</a>
<a class="sidebar-link admin-link {% if active_nav == 'cron' %}active{% endif %}" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link {% if active_nav == 'system_logs' %}active{% endif %}" href="/system-logs" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>系统日志</a>
<a class="sidebar-link admin-link {% if active_nav == 'admin' %}active{% endif %}" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>用户管理</a>
</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');
$('oldPwd').value = '';
$('newPwd').value = '';
$('cfmPwd').value = '';
$('pwdMsg').textContent = '';
$('pwdMsg').className = 'modal-msg';
}
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='/';
}
// 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>