This commit is contained in:
aaron 2026-04-22 11:03:24 +08:00
parent 491a1d29f1
commit b1e6215ae2
11 changed files with 1041 additions and 419 deletions

View File

@ -28,6 +28,162 @@ def _parse_signal_timestamp(value: Any) -> datetime | None:
return None
def _safe_float(value: Any, default: float = 0.0) -> float:
try:
if value is None or value == "":
return default
return float(value)
except (TypeError, ValueError):
return default
def _normalize_platform_position(platform: str, position: Dict[str, Any]) -> Dict[str, Any]:
side_raw = str(position.get("side") or "").lower()
side = "long" if side_raw in {"buy", "long"} else "short"
symbol = position.get("symbol") or position.get("coin") or "-"
if isinstance(symbol, str) and symbol.endswith("USDTUSDT"):
symbol = symbol.replace("USDTUSDT", "USDT")
entry_price = _safe_float(position.get("entry_price") or position.get("filled_price"))
mark_price = _safe_float(position.get("mark_price") or position.get("current_price"))
if mark_price <= 0:
mark_price = entry_price
size = abs(_safe_float(position.get("size") or position.get("quantity")))
leverage = _safe_float(position.get("leverage"))
margin = _safe_float(position.get("margin") or position.get("initialMargin") or position.get("initial_margin"))
unrealized_pnl = _safe_float(position.get("unrealized_pnl") or position.get("pnl_amount"))
pnl_percent = _safe_float(position.get("unrealized_pnl_pct") or position.get("pnl_percent") or position.get("percentage"))
return {
"platform": platform,
"symbol": symbol,
"side": side,
"size": size,
"entry_price": entry_price,
"mark_price": mark_price,
"leverage": leverage,
"margin": margin,
"unrealized_pnl": unrealized_pnl,
"pnl_percent": pnl_percent,
"take_profit": position.get("take_profit"),
"stop_loss": position.get("stop_loss"),
"liquidation_price": position.get("liquidation_price"),
"opened_at": position.get("opened_at") or position.get("created_at"),
}
def _normalize_platform_order(platform: str, order: Dict[str, Any]) -> Dict[str, Any]:
side_raw = str(order.get("side") or "").lower()
side = "long" if side_raw in {"buy", "long", "b"} else "short"
symbol = order.get("symbol") or order.get("coin") or "-"
if isinstance(symbol, str) and symbol.endswith("USDTUSDT"):
symbol = symbol.replace("USDTUSDT", "USDT")
price = _safe_float(order.get("price") or order.get("entry_price"))
size = abs(_safe_float(order.get("size") or order.get("quantity")))
order_type = str(order.get("order_type") or order.get("entry_type") or order.get("type") or "").lower()
status = str(order.get("status") or "").lower()
is_reduce_only = bool(order.get("is_reduce_only"))
category = "tp_sl" if is_reduce_only else "entry"
if platform == "paper" and status == "pending":
category = "entry"
return {
"platform": platform,
"symbol": symbol,
"side": side,
"category": category,
"price": price,
"size": size,
"leverage": _safe_float(order.get("leverage")),
"margin": _safe_float(order.get("margin")),
"order_type": order_type,
"status": status,
"stop_loss": order.get("stop_loss"),
"take_profit": order.get("take_profit"),
"signal_grade": order.get("signal_grade"),
"signal_type": order.get("signal_type"),
"confidence": _safe_float(order.get("confidence")),
"created_at": order.get("created_at") or order.get("timestamp"),
}
def _build_attention_items(
platform_halts: Dict[str, Any],
platforms: Dict[str, Any],
unified_positions: list[Dict[str, Any]],
unified_orders: list[Dict[str, Any]],
execution_events: list[Dict[str, Any]],
) -> list[Dict[str, Any]]:
items: list[Dict[str, Any]] = []
for platform, halt in (platform_halts or {}).items():
if halt and halt.get("halted"):
items.append({
"severity": "danger",
"title": f"{platform} 已停机",
"detail": halt.get("reason") or "平台已触发停机/熔断",
"timestamp": halt.get("halted_at"),
})
for platform_name, payload in (platforms or {}).items():
if payload.get("enabled") is False:
continue
risk = payload.get("risk") or {}
current_leverage = _safe_float(risk.get("current_leverage"))
max_leverage = _safe_float(risk.get("max_leverage"))
drawdown_pct = _safe_float(risk.get("drawdown_percent") or risk.get("drawdown"))
breaker_pct = _safe_float(risk.get("circuit_breaker_threshold"), 25.0)
if breaker_pct > 0 and drawdown_pct >= breaker_pct * 0.7:
items.append({
"severity": "warning",
"title": f"{platform_name} 回撤接近熔断",
"detail": f"当前回撤 {drawdown_pct:.1f}% / 阈值 {breaker_pct:.1f}%",
"timestamp": None,
})
if max_leverage > 0 and current_leverage >= max_leverage * 0.8:
items.append({
"severity": "warning",
"title": f"{platform_name} 杠杆占用偏高",
"detail": f"当前总杠杆 {current_leverage:.2f}x / 上限 {max_leverage:.2f}x",
"timestamp": None,
})
for pos in unified_positions:
if not pos.get("stop_loss") or not pos.get("take_profit"):
items.append({
"severity": "warning",
"title": f"{pos['platform']} {pos['symbol']} 风控不完整",
"detail": "持仓缺少止盈或止损,请确认执行层保护单状态",
"timestamp": pos.get("opened_at"),
})
pending_entry_count = sum(1 for order in unified_orders if order.get("category") == "entry")
if pending_entry_count > 0:
items.append({
"severity": "info",
"title": "存在待成交入场单",
"detail": f"当前共有 {pending_entry_count} 笔待成交入场单,建议关注价格接近度和资金占用。",
"timestamp": None,
})
for event in (execution_events or [])[:8]:
if event.get("status") in {"error", "warning"}:
items.append({
"severity": event.get("status"),
"title": f"{event.get('platform', '-')}: {event.get('event_type', '-')}",
"detail": event.get("reason") or "最近执行事件需要关注",
"timestamp": event.get("timestamp"),
})
return items[:12]
@router.get("/status", response_model=Dict[str, Any])
async def get_system_status():
"""
@ -221,41 +377,101 @@ async def get_console_snapshot():
if (_parse_signal_timestamp(signal.get("created_at")) or datetime.min) >= recent_cutoff
)
paper_position_items = [
_normalize_platform_position("paper", pos)
for pos in paper_service.get_open_positions()[:12]
]
paper_order_items = [
_normalize_platform_order("paper", order)
for order in paper_pending[:12]
]
bitget_position_items = [
_normalize_platform_position("bitget", pos)
for pos in (bg_positions[:12] if bitget_service is not None else [])
]
bitget_order_items = [
_normalize_platform_order("bitget", order)
for order in (bg_orders[:12] if bitget_service is not None else [])
]
hyperliquid_position_items = [
_normalize_platform_position("hyperliquid", pos)
for pos in (hl_positions[:12] if hyperliquid_service is not None else [])
]
hyperliquid_order_items = [
_normalize_platform_order("hyperliquid", order)
for order in (hl_orders[:12] if hyperliquid_service is not None else [])
]
unified_positions = sorted(
paper_position_items + bitget_position_items + hyperliquid_position_items,
key=lambda item: _parse_signal_timestamp(item.get("opened_at")) or datetime.min,
reverse=True,
)
unified_orders = sorted(
paper_order_items + bitget_order_items + hyperliquid_order_items,
key=lambda item: _parse_signal_timestamp(item.get("created_at")) or datetime.min,
reverse=True,
)
platforms_payload = {
"paper": {
"enabled": True,
"account": paper_account,
"positions": {
"count": len(paper_positions),
"items": paper_positions[:8],
},
"orders": {
"count": len(paper_orders),
"pending_count": len(paper_pending),
"items": paper_pending[:8],
},
"statistics": {
"win_rate": paper_stats.get("win_rate", 0),
"total_trades": paper_stats.get("total_trades", 0),
"total_pnl": paper_stats.get("total_pnl", 0),
"max_drawdown": paper_stats.get("max_drawdown", 0),
"by_grade": paper_stats.get("by_grade", {}),
},
"risk": {
"current_leverage": paper_account.get("current_total_leverage", 0),
"max_leverage": paper_account.get("max_total_leverage", 0),
"drawdown_percent": paper_account.get("max_drawdown", 0),
"circuit_breaker_threshold": 25,
},
},
"bitget": bitget_summary,
"hyperliquid": hyperliquid_summary,
}
execution_events = crypto_agent.get_recent_execution_events(limit=40)
attention_items = _build_attention_items(
crypto_status.get("platform_halts", {}),
platforms_payload,
unified_positions,
unified_orders,
execution_events,
)
return {
"status": "success",
"data": {
"generated_at": now.isoformat(),
"system": summary,
"crypto_agent": crypto_status,
"execution_events": crypto_agent.get_recent_execution_events(limit=40),
"execution_events": execution_events,
"signals": {
"stats_7d": signal_stats,
"latest": latest_signals,
"recent_30m_count": recent_signal_count,
},
"platforms": {
"paper": {
"enabled": True,
"account": paper_account,
"positions": {
"count": len(paper_positions),
"items": paper_positions[:8],
},
"orders": {
"count": len(paper_orders),
"pending_count": len(paper_pending),
"items": paper_pending[:8],
},
"statistics": {
"win_rate": paper_stats.get("win_rate", 0),
"total_trades": paper_stats.get("total_trades", 0),
"total_pnl": paper_stats.get("total_pnl", 0),
"max_drawdown": paper_stats.get("max_drawdown", 0),
"by_grade": paper_stats.get("by_grade", {}),
},
},
"bitget": bitget_summary,
"hyperliquid": hyperliquid_summary,
"platforms": platforms_payload,
"management": {
"positions": unified_positions[:18],
"orders": unified_orders[:24],
"attention_items": attention_items,
},
}
}

View File

@ -11,7 +11,7 @@
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Page-Specific Styles -->
<style>
@ -25,11 +25,13 @@
/* Sidebar */
.admin-sidebar {
width: 240px;
background: var(--bg-primary);
background: var(--panel-strong);
border-right: 1px solid var(--border);
padding: 24px 0;
position: fixed;
height: 100vh;
backdrop-filter: blur(18px);
box-shadow: var(--shadow-soft);
}
.admin-logo {
@ -63,7 +65,7 @@
}
.admin-nav-item.active {
background: var(--primary-light);
background: rgba(126, 200, 255, 0.12);
color: var(--primary);
border-left-color: var(--primary);
font-weight: 500;
@ -91,7 +93,7 @@
.logout-btn {
padding: 10px 20px;
background: var(--bg-primary);
background: rgba(126, 200, 255, 0.08);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text-secondary);
@ -120,6 +122,8 @@
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 24px;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.stat-label {
@ -165,18 +169,19 @@
.search-btn {
padding: 12px 24px;
background: var(--primary);
background: linear-gradient(135deg, #8fd7ff, #63e6be);
border: none;
border-radius: var(--radius-md);
color: white;
color: #071018;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 10px 24px rgba(49, 132, 189, 0.22);
}
.search-btn:hover {
background: var(--primary-dark);
transform: translateY(-1px);
}
/* Table */
@ -185,6 +190,8 @@
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
table {
@ -249,7 +256,7 @@
.pagination button {
padding: 8px 16px;
background: var(--bg-primary);
background: rgba(126, 200, 255, 0.08);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text-primary);
@ -259,7 +266,7 @@
}
.pagination button:hover:not(:disabled) {
background: var(--primary-light);
background: rgba(126, 200, 255, 0.14);
border-color: var(--primary);
color: var(--primary);
}
@ -281,20 +288,22 @@
left: 0;
width: 100%;
height: 100%;
background: var(--bg-secondary);
background: rgba(4, 9, 14, 0.82);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(10px);
}
.login-box {
width: 100%;
max-width: 400px;
background: var(--bg-primary);
background: var(--panel-strong);
border-radius: var(--radius-lg);
padding: 48px;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border);
}
.login-box h2 {
@ -329,10 +338,10 @@
.login-box button {
width: 100%;
padding: 14px;
background: var(--primary);
background: linear-gradient(135deg, #8fd7ff, #63e6be);
border: none;
border-radius: var(--radius-md);
color: white;
color: #071018;
font-size: 15px;
font-weight: 600;
cursor: pointer;
@ -340,7 +349,7 @@
}
.login-box button:hover {
background: var(--primary-dark);
transform: translateY(-1px);
}
.error-message {

View File

@ -723,6 +723,152 @@
font-family: "IBM Plex Mono", monospace;
}
.ops-grid {
display: grid;
gap: 18px;
grid-template-columns: 0.9fr 1.1fr;
margin-top: 18px;
}
.attention-list,
.unified-list {
display: grid;
gap: 10px;
}
.attention-item,
.unified-item {
padding: 14px 16px;
border-radius: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.05);
}
.attention-item.danger {
border-color: rgba(255, 111, 97, 0.2);
background: rgba(255, 111, 97, 0.08);
}
.attention-item.warning {
border-color: rgba(255, 184, 77, 0.2);
background: rgba(255, 184, 77, 0.08);
}
.attention-item.info {
border-color: rgba(126, 200, 255, 0.2);
background: rgba(126, 200, 255, 0.08);
}
.attention-title,
.unified-title {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
margin-bottom: 6px;
}
.attention-title strong,
.unified-title strong {
font-size: 14px;
}
.attention-time,
.unified-time {
color: var(--muted);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
}
.attention-detail,
.unified-detail {
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
.table-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.toolbar-chip {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border-radius: 999px;
background: rgba(126, 200, 255, 0.08);
color: var(--cold);
border: 1px solid rgba(126, 200, 255, 0.16);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.unified-section {
margin-top: 18px;
}
.unified-table {
overflow-x: auto;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.05);
background: rgba(255,255,255,0.02);
}
.unified-table table {
min-width: 980px;
}
.platform-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 92px;
padding: 5px 10px;
border-radius: 999px;
font-size: 10px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--cold);
background: rgba(126, 200, 255, 0.1);
border: 1px solid rgba(126, 200, 255, 0.2);
}
.side-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 60px;
padding: 4px 10px;
border-radius: 999px;
font-size: 10px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.side-pill.long {
color: var(--good);
background: rgba(48, 209, 88, 0.12);
border: 1px solid rgba(48, 209, 88, 0.2);
}
.side-pill.short {
color: var(--danger);
background: rgba(255, 111, 97, 0.12);
border: 1px solid rgba(255, 111, 97, 0.2);
}
.inline-mono {
font-family: "IBM Plex Mono", monospace;
font-size: 12px;
}
.loading,
.error-box,
.empty-box {
@ -747,7 +893,8 @@
}
.platform-grid,
.signal-grid {
.signal-grid,
.ops-grid {
grid-template-columns: 1fr;
}
@ -838,6 +985,18 @@
</div>
</section>
<section class="panel">
<div class="panel-header">
<div>
<h2 class="panel-title">管理待处理事项</h2>
<div class="panel-sub">风险、停机、异常、待成交提醒</div>
</div>
</div>
<div class="attention-list" id="attentionList">
<div class="loading">正在汇总待处理事项...</div>
</div>
</section>
<section class="panel">
<div class="panel-header">
<div>
@ -909,6 +1068,34 @@
</section>
</div>
</section>
<section class="ops-grid">
<section class="panel unified-section">
<div class="panel-header">
<div>
<h2 class="panel-title">统一持仓视图</h2>
<div class="panel-sub">三端持仓合并,优先看风险与盈亏</div>
</div>
</div>
<div class="table-toolbar" id="positionsToolbar"></div>
<div class="unified-table" id="positionsTable">
<div class="loading">正在整理跨平台持仓...</div>
</div>
</section>
<section class="panel unified-section">
<div class="panel-header">
<div>
<h2 class="panel-title">统一挂单视图</h2>
<div class="panel-sub">入场单、保护单、资金占用一屏观察</div>
</div>
</div>
<div class="table-toolbar" id="ordersToolbar"></div>
<div class="unified-table" id="ordersTable">
<div class="loading">正在整理跨平台挂单...</div>
</div>
</section>
</section>
</div>
<script>
@ -1300,6 +1487,122 @@
`).join('');
}
function renderAttentionItems(items) {
const container = document.getElementById('attentionList');
if (!items || items.length === 0) {
container.innerHTML = '<div class="empty-box">当前没有需要人工处理的事项</div>';
return;
}
container.innerHTML = items.map((item) => `
<div class="attention-item ${item.severity || 'info'}">
<div class="attention-title">
<strong>${item.title || '-'}</strong>
<span class="attention-time">${item.timestamp ? relativeTime(item.timestamp) : 'now'}</span>
</div>
<div class="attention-detail">${item.detail || '-'}</div>
</div>
`).join('');
}
function renderUnifiedPositions(positions) {
const toolbar = document.getElementById('positionsToolbar');
const container = document.getElementById('positionsTable');
const total = positions || [];
const platformCounts = ['paper', 'bitget', 'hyperliquid'].map((platform) => {
const count = total.filter((item) => item.platform === platform).length;
return `<span class="toolbar-chip">${platform}: ${count}</span>`;
}).join('');
toolbar.innerHTML = `${platformCounts}<span class="toolbar-chip">total: ${total.length}</span>`;
if (!total.length) {
container.innerHTML = '<div class="empty-box">当前没有跨平台持仓</div>';
return;
}
container.innerHTML = `
<table>
<thead>
<tr>
<th>平台</th>
<th>交易对</th>
<th>方向</th>
<th>入场 / 现价</th>
<th>仓位 / 杠杆</th>
<th>止盈 / 止损</th>
<th>未实现盈亏</th>
<th>盈亏比例</th>
<th>时间</th>
</tr>
</thead>
<tbody>
${total.map((item) => `
<tr>
<td><span class="platform-pill">${item.platform}</span></td>
<td><strong>${item.symbol || '-'}</strong></td>
<td><span class="side-pill ${item.side === 'long' ? 'long' : 'short'}">${item.side === 'long' ? 'long' : 'short'}</span></td>
<td class="inline-mono">${formatMoney(item.entry_price)} / ${formatMoney(item.mark_price)}</td>
<td class="inline-mono">${formatNumber(item.size, 4)} / ${formatNumber(item.leverage, 1)}x</td>
<td class="inline-mono">${item.take_profit ? formatMoney(item.take_profit) : '-'} / ${item.stop_loss ? formatMoney(item.stop_loss) : '-'}</td>
<td style="color:${(item.unrealized_pnl || 0) >= 0 ? 'var(--good)' : 'var(--danger)'}">${formatMoney(item.unrealized_pnl)}</td>
<td style="color:${(item.pnl_percent || 0) >= 0 ? 'var(--good)' : 'var(--danger)'}">${formatPercent(item.pnl_percent, 2)}</td>
<td class="inline-mono">${item.opened_at ? relativeTime(item.opened_at) : '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
function renderUnifiedOrders(orders) {
const toolbar = document.getElementById('ordersToolbar');
const container = document.getElementById('ordersTable');
const total = orders || [];
const entryCount = total.filter((item) => item.category === 'entry').length;
const protectionCount = total.filter((item) => item.category === 'tp_sl').length;
toolbar.innerHTML = `
<span class="toolbar-chip">entry: ${entryCount}</span>
<span class="toolbar-chip">tp/sl: ${protectionCount}</span>
<span class="toolbar-chip">total: ${total.length}</span>
`;
if (!total.length) {
container.innerHTML = '<div class="empty-box">当前没有跨平台挂单</div>';
return;
}
container.innerHTML = `
<table>
<thead>
<tr>
<th>平台</th>
<th>交易对</th>
<th>方向</th>
<th>类别</th>
<th>价格</th>
<th>数量 / 杠杆</th>
<th>信号</th>
<th>时间</th>
</tr>
</thead>
<tbody>
${total.map((item) => `
<tr>
<td><span class="platform-pill">${item.platform}</span></td>
<td><strong>${item.symbol || '-'}</strong></td>
<td><span class="side-pill ${item.side === 'long' ? 'long' : 'short'}">${item.side === 'long' ? 'long' : 'short'}</span></td>
<td class="inline-mono">${item.category === 'tp_sl' ? 'TP/SL' : 'ENTRY'}</td>
<td class="inline-mono">${formatMoney(item.price)}</td>
<td class="inline-mono">${formatNumber(item.size, 4)} / ${item.leverage ? `${formatNumber(item.leverage, 1)}x` : '-'}</td>
<td class="inline-mono">${item.signal_grade || '-'} ${item.signal_type || ''} ${item.confidence ? `/ ${formatPercent(item.confidence, 1)}` : ''}</td>
<td class="inline-mono">${item.created_at ? relativeTime(item.created_at) : '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
async function resumePlatform(platform, button) {
const platformMap = {
PaperTrading: 'PaperTrading',
@ -1351,6 +1654,9 @@
renderDecisionPreview(data.crypto_agent?.last_execution_preview || {});
renderHalts(data.crypto_agent?.platform_halts || {});
renderExecutionEvents(data.execution_events || []);
renderAttentionItems(data.management?.attention_items || []);
renderUnifiedPositions(data.management?.positions || []);
renderUnifiedOrders(data.management?.orders || []);
} catch (error) {
console.error(error);
setFeedback(`总控台加载失败: ${error.message}`, true);

View File

@ -1,47 +1,58 @@
/* ========================================
TRADUS - 清新简洁设计系统
========================================
Design: Modern, Clean, Minimal
Color: Professional Blue & White
XClaw Console Theme
======================================== */
/* CSS Variables - Clean Color Palette */
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
:root {
/* Primary Colors */
--primary: #0066FF;
--primary-light: #E8F0FF;
--primary-dark: #0052CC;
/* Console core palette */
--bg: #08111a;
--bg-elevated: #0d1823;
--panel: rgba(11, 21, 32, 0.86);
--panel-strong: rgba(8, 18, 28, 0.96);
--panel-soft: rgba(16, 28, 41, 0.74);
--line: rgba(128, 169, 202, 0.18);
--line-strong: rgba(128, 169, 202, 0.32);
--text: #edf6ff;
--muted: #8ea6bc;
--cold: #7ec8ff;
--accent: #9cff57;
--warn: #ffb84d;
--danger: #ff6f61;
--good: #30d158;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.38);
--shadow-soft: 0 16px 48px rgba(0, 0, 0, 0.24);
--glow-cold: 0 0 0 1px rgba(126, 200, 255, 0.2), 0 14px 38px rgba(18, 51, 78, 0.3);
/* Background Colors */
--bg-primary: #FFFFFF;
--bg-secondary: #F8FAFB;
--bg-tertiary: #F1F5F9;
/* Compatibility mapping */
--primary: var(--cold);
--primary-light: rgba(126, 200, 255, 0.14);
--primary-dark: #4eaef2;
/* Text Colors */
--text-primary: #1E293B;
--text-secondary: #64748B;
--text-tertiary: #94A3B8;
--bg-primary: rgba(11, 21, 32, 0.86);
--bg-secondary: rgba(16, 28, 41, 0.72);
--bg-tertiary: rgba(26, 41, 58, 0.86);
/* Semantic Colors */
--success: #10B981;
--success-light: #D1FAE5;
--error: #EF4444;
--error-light: #FEE2E2;
--warning: #F59E0B;
--warning-light: #FEF3C7;
--info: #3B82F6;
--info-light: #DBEAFE;
--text-primary: var(--text);
--text-secondary: var(--muted);
--text-tertiary: #6f8598;
/* Borders */
--border: #E2E8F0;
--border-light: #F1F5F9;
--success: var(--good);
--success-light: rgba(48, 209, 88, 0.14);
--error: var(--danger);
--error-light: rgba(255, 111, 97, 0.14);
--warning: var(--warn);
--warning-light: rgba(255, 184, 77, 0.14);
--info: var(--cold);
--info-light: rgba(126, 200, 255, 0.14);
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
--border: var(--line);
--border-light: rgba(128, 169, 202, 0.1);
--shadow-sm: 0 10px 24px rgba(0, 0, 0, 0.18);
--shadow-md: var(--shadow-soft);
--shadow-lg: var(--shadow);
/* Spacing */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
@ -49,55 +60,78 @@
--space-xl: 32px;
--space-2xl: 48px;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-sm: 10px;
--radius-md: 16px;
--radius-lg: 22px;
--radius-xl: 28px;
/* Typography */
--font-xs: 11px;
--font-sm: 13px;
--font-base: 14px;
--font-md: 16px;
--font-lg: 18px;
--font-xl: 24px;
--font-2xl: 28px;
--font-3xl: 32px;
--font-2xl: 30px;
--font-3xl: 38px;
}
/* ========================================
RESET & BASE
======================================== */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-secondary);
html,
body {
min-height: 100%;
}
html {
background:
radial-gradient(circle at top left, rgba(64, 127, 255, 0.16), transparent 28%),
radial-gradient(circle at top right, rgba(156, 255, 87, 0.12), transparent 22%),
linear-gradient(180deg, #071018 0%, #08111a 48%, #050b11 100%);
}
body {
font-family: "IBM Plex Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
background: transparent;
color: var(--text-primary);
font-size: var(--font-base);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
position: relative;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px);
background-size: 28px 28px;
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.38), rgba(0, 0, 0, 0.92));
}
#app {
min-height: 100vh;
position: relative;
z-index: 1;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
line-height: 1.3;
line-height: 1.2;
color: var(--text-primary);
letter-spacing: -0.02em;
}
h1 { font-size: var(--font-3xl); }
@ -107,16 +141,25 @@ h3 { font-size: var(--font-xl); }
a {
color: var(--primary);
text-decoration: none;
transition: color 0.2s;
transition: color 0.2s ease, opacity 0.2s ease;
}
a:hover {
color: var(--primary-dark);
}
/* ========================================
UTILITY CLASSES
======================================== */
button,
input,
select,
textarea {
font: inherit;
}
code,
pre,
.mono {
font-family: "IBM Plex Mono", monospace;
}
.text-center { text-align: center; }
.text-right { text-align: right; }
@ -132,91 +175,122 @@ a:hover {
.font-bold { font-weight: 600; }
.font-semibold { font-weight: 500; }
/* ========================================
COMPONENTS
======================================== */
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 20px;
border-radius: var(--radius-md);
font-size: 14px;
font-weight: 600;
min-height: 42px;
padding: 10px 18px;
border-radius: 14px;
border: 1px solid transparent;
background: rgba(126, 200, 255, 0.1);
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s;
border: none;
font-family: inherit;
transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.02em;
backdrop-filter: blur(14px);
}
.btn:hover:not(:disabled) {
transform: translateY(-1px);
border-color: rgba(126, 200, 255, 0.24);
box-shadow: var(--shadow-sm);
}
.btn:disabled {
opacity: 0.48;
cursor: not-allowed;
}
.btn-primary {
background: var(--primary);
color: white;
background: linear-gradient(135deg, #8fd7ff, #63e6be);
color: #071018;
border-color: rgba(126, 200, 255, 0.34);
box-shadow: 0 10px 30px rgba(49, 132, 189, 0.22);
}
.btn-primary:hover {
background: var(--primary-dark);
.btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, #9ce0ff, #7aeecb);
}
.btn-secondary {
background: var(--bg-primary);
background: rgba(126, 200, 255, 0.08);
border-color: var(--border);
color: var(--primary);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: var(--primary-light);
border-color: var(--primary);
.btn-secondary:hover:not(:disabled) {
background: rgba(126, 200, 255, 0.14);
}
.btn-danger {
background: var(--error-light);
background: rgba(255, 111, 97, 0.12);
color: var(--error);
border: 1px solid var(--error);
border-color: rgba(255, 111, 97, 0.22);
}
.btn-danger:hover {
background: var(--error);
color: white;
.btn-danger:hover:not(:disabled) {
background: rgba(255, 111, 97, 0.18);
}
.btn-success {
background: var(--success-light);
background: rgba(48, 209, 88, 0.12);
color: var(--success);
border: 1px solid var(--success);
border-color: rgba(48, 209, 88, 0.2);
}
.btn-success:hover {
background: var(--success);
color: white;
.btn-success:hover:not(:disabled) {
background: rgba(48, 209, 88, 0.18);
}
.btn-small {
min-height: 34px;
padding: 6px 12px;
font-size: 12px;
border-radius: 12px;
}
/* Cards */
.card {
.card,
.table-container {
position: relative;
overflow: hidden;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.card::after,
.table-container::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.04), transparent 22%, transparent 72%, rgba(158, 214, 255, 0.04));
}
.card {
padding: 20px;
transition: all 0.2s;
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
border-color: var(--primary);
box-shadow: var(--shadow-md);
transform: translateY(-1px);
border-color: var(--line-strong);
box-shadow: var(--shadow);
}
/* Badges */
.badge {
display: inline-block;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: var(--radius-sm);
border-radius: 999px;
border: 1px solid transparent;
font-size: 12px;
font-weight: 600;
}
@ -224,34 +298,31 @@ a:hover {
.badge-success {
background: var(--success-light);
color: var(--success);
border-color: rgba(48, 209, 88, 0.2);
}
.badge-error {
background: var(--error-light);
color: var(--error);
border-color: rgba(255, 111, 97, 0.22);
}
.badge-warning {
background: var(--warning-light);
color: var(--warning);
border-color: rgba(255, 184, 77, 0.22);
}
.badge-info {
background: var(--info-light);
color: var(--info);
border-color: rgba(126, 200, 255, 0.22);
}
.badge-neutral {
background: var(--bg-tertiary);
background: rgba(128, 169, 202, 0.12);
color: var(--text-secondary);
}
/* Tables */
.table-container {
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
border-color: var(--border);
}
table {
@ -260,15 +331,15 @@ table {
}
th {
background: var(--bg-tertiary);
background: rgba(20, 34, 48, 0.92);
padding: 14px 16px;
text-align: left;
font-size: 12px;
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
border-bottom: 1px solid var(--border);
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.08em;
}
td {
@ -283,39 +354,49 @@ tr:last-child td {
}
tr:hover {
background: var(--bg-secondary);
background: rgba(126, 200, 255, 0.05);
}
/* Input Fields */
.input {
.input,
input[type="text"],
input[type="number"],
input[type="password"],
input[type="tel"],
input[type="email"],
input[type="search"],
select,
textarea {
width: 100%;
padding: 12px 16px;
background: var(--bg-secondary);
background: rgba(10, 20, 31, 0.78);
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-size: 14px;
border-radius: 14px;
color: var(--text-primary);
font-family: inherit;
transition: all 0.2s;
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
backdrop-filter: blur(10px);
}
.input:focus {
outline: none;
background: var(--bg-primary);
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.input::placeholder {
.input::placeholder,
input::placeholder,
textarea::placeholder {
color: var(--text-tertiary);
}
/* Spinner */
.input:focus,
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: rgba(126, 200, 255, 0.34);
box-shadow: 0 0 0 3px rgba(126, 200, 255, 0.12);
background: rgba(12, 24, 36, 0.9);
}
.spinner {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid var(--bg-tertiary);
border: 3px solid rgba(128, 169, 202, 0.14);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
@ -325,105 +406,148 @@ tr:hover {
to { transform: rotate(360deg); }
}
/* ========================================
LAYOUT
======================================== */
.container {
max-width: 1400px;
width: min(1440px, calc(100vw - 32px));
margin: 0 auto;
padding: 32px 24px;
padding: 24px 0 40px;
position: relative;
z-index: 1;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
gap: 16px;
margin-bottom: 24px;
padding: 22px 24px;
background: var(--panel-strong);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.page-title {
font-size: var(--font-2xl);
display: flex;
align-items: center;
gap: 12px;
font-size: clamp(28px, 4vw, 40px);
font-weight: 700;
letter-spacing: -0.04em;
color: var(--text-primary);
}
.page-subtitle {
font-size: var(--font-base);
margin-top: 6px;
color: var(--text-secondary);
margin-top: 4px;
font-size: 14px;
line-height: 1.6;
}
/* Grid Layout */
.grid {
display: grid;
gap: 16px;
}
.grid-cols-2 {
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-cols-4 {
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.grid-auto-fit {
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
/* Tabs */
.tabs {
display: flex;
border-bottom: 1px solid var(--border);
gap: 8px;
margin-bottom: 24px;
padding: 8px;
background: rgba(10, 20, 31, 0.72);
border: 1px solid var(--border);
border-radius: 18px;
overflow-x: auto;
}
.tab {
padding: 14px 24px;
padding: 12px 18px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
border: 1px solid transparent;
border-radius: 12px;
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
white-space: nowrap;
transition: all 0.2s ease;
}
.tab:hover {
color: var(--text-primary);
background: rgba(126, 200, 255, 0.08);
}
.tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
color: #071018;
background: linear-gradient(135deg, #8fd7ff, #63e6be);
border-color: rgba(126, 200, 255, 0.26);
box-shadow: 0 8px 20px rgba(49, 132, 189, 0.24);
}
/* ========================================
RESPONSIVE
======================================== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(8, 18, 28, 0.82);
}
::-webkit-scrollbar-thumb {
background: rgba(128, 169, 202, 0.26);
border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(128, 169, 202, 0.42);
}
::selection {
background: rgba(126, 200, 255, 0.22);
color: var(--text-primary);
}
:focus-visible {
outline: 2px solid rgba(126, 200, 255, 0.52);
outline-offset: 2px;
}
@media (max-width: 768px) {
:root {
--font-xl: 20px;
--font-2xl: 24px;
--font-3xl: 28px;
--font-3xl: 30px;
--radius-md: 14px;
--radius-lg: 18px;
}
.container {
padding: 20px 16px;
width: min(100vw - 20px, 100%);
padding: 16px 0 28px;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 18px;
}
.grid-cols-2,
@ -432,63 +556,25 @@ tr:hover {
grid-template-columns: 1fr;
}
.tabs {
overflow-x: auto;
}
.tab {
padding: 10px 16px;
font-size: 13px;
white-space: nowrap;
}
.table-container {
overflow-x: auto;
}
table {
min-width: 800px;
min-width: 720px;
}
}
@media (max-width: 480px) {
:root {
--font-xl: 18px;
--font-2xl: 20px;
--font-3xl: 24px;
.container {
width: min(100vw - 16px, 100%);
}
.page-header {
border-radius: 16px;
}
.page-title {
font-size: 26px;
}
}
/* ========================================
SCROLLBAR
======================================== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* Selection */
::selection {
background: var(--primary-light);
color: var(--primary);
}
/* Focus */
:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}

View File

@ -7,7 +7,7 @@
<link rel="stylesheet" href="/static/css/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<style>
html, body {
@ -21,19 +21,20 @@
.hyperliquid-page {
min-height: 100vh;
background: var(--bg-secondary);
background: transparent;
}
.paper-badge {
display: inline-block;
background: var(--primary);
color: white;
padding: 4px 12px;
border-radius: var(--radius-sm);
background: linear-gradient(135deg, #8fd7ff, #63e6be);
color: #071018;
padding: 5px 12px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 1px;
margin-left: 12px;
box-shadow: 0 10px 24px rgba(49, 132, 189, 0.22);
}
.header-actions {
@ -86,6 +87,8 @@
border-radius: var(--radius-md);
padding: var(--space-lg);
transition: all 0.2s;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.stat-card:hover {
@ -107,7 +110,7 @@
font-weight: 700;
color: var(--text-primary);
margin-bottom: var(--space-xs);
font-family: 'Inter', monospace;
font-family: "IBM Plex Mono", monospace;
}
.stat-value.positive {
@ -139,6 +142,8 @@
border-radius: var(--radius-md);
overflow: hidden;
transition: all 0.2s;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.panel:hover {
@ -152,7 +157,7 @@
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
background: rgba(10, 20, 31, 0.76);
}
.panel-title {
@ -184,7 +189,7 @@
}
.list-item:hover {
background: var(--bg-secondary);
background: rgba(126, 200, 255, 0.05);
}
.item-left {
@ -325,7 +330,7 @@
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
background: var(--bg-secondary);
background: rgba(10, 20, 31, 0.76);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}

View File

@ -11,7 +11,7 @@
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Marked.js for Markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
@ -26,12 +26,14 @@
/* Header */
.header {
background: var(--bg-primary);
background: var(--panel-strong);
border-bottom: 1px solid var(--border);
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
backdrop-filter: blur(18px);
box-shadow: var(--shadow-soft);
}
.logo {
@ -58,8 +60,9 @@
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--bg-secondary);
border-radius: 8px;
background: rgba(126, 200, 255, 0.08);
border-radius: 14px;
border: 1px solid var(--border);
font-size: 13px;
color: var(--text-secondary);
}
@ -70,12 +73,13 @@
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
background: rgba(126, 200, 255, 0.08);
border: none;
border-radius: 8px;
border-radius: 12px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
border: 1px solid var(--border);
}
.logout-btn:hover {
@ -113,6 +117,7 @@
font-weight: 700;
color: var(--text-primary);
margin-bottom: 16px;
letter-spacing: -0.04em;
}
.welcome-subtitle {
@ -133,15 +138,16 @@
padding: 14px 20px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
border-radius: 14px;
font-size: 14px;
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s;
box-shadow: var(--shadow-soft);
}
.example-btn:hover {
background: var(--primary-light);
background: rgba(126, 200, 255, 0.14);
border-color: var(--primary);
color: var(--primary);
}
@ -163,13 +169,16 @@
.message-content {
padding: 16px 20px;
border-radius: 12px;
border-radius: 18px;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(14px);
}
.message.user .message-content {
background: var(--primary);
color: white;
background: linear-gradient(135deg, rgba(126, 200, 255, 0.26), rgba(99, 230, 190, 0.18));
color: var(--text-primary);
margin-left: 48px;
border: 1px solid rgba(126, 200, 255, 0.22);
}
.message.assistant .message-content {
@ -187,9 +196,9 @@
.action-btn {
padding: 6px 12px;
background: var(--bg-primary);
background: rgba(126, 200, 255, 0.08);
border: 1px solid var(--border);
border-radius: 6px;
border-radius: 12px;
font-size: 13px;
color: var(--text-secondary);
cursor: pointer;
@ -200,7 +209,7 @@
}
.action-btn:hover {
background: var(--bg-tertiary);
background: rgba(126, 200, 255, 0.14);
color: var(--primary);
border-color: var(--primary);
}
@ -233,7 +242,8 @@
.input-container {
border-top: 1px solid var(--border);
padding: 20px 24px;
background: var(--bg-primary);
background: var(--panel-strong);
backdrop-filter: blur(18px);
}
.input-wrapper {
@ -247,9 +257,9 @@
textarea {
flex: 1;
padding: 12px 16px;
background: var(--bg-secondary);
background: rgba(10, 20, 31, 0.78);
border: 1px solid var(--border);
border-radius: 8px;
border-radius: 16px;
font-size: 15px;
font-family: inherit;
color: var(--text-primary);
@ -259,7 +269,7 @@
textarea:focus {
outline: none;
background: var(--bg-primary);
background: rgba(12, 24, 36, 0.92);
border-color: var(--primary);
}
@ -270,20 +280,21 @@
.send-btn {
width: 40px;
height: 40px;
background: var(--primary);
background: linear-gradient(135deg, #8fd7ff, #63e6be);
border: none;
border-radius: 8px;
color: white;
border-radius: 14px;
color: #071018;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
box-shadow: 0 10px 24px rgba(49, 132, 189, 0.24);
}
.send-btn:hover:not(:disabled) {
background: var(--primary-dark);
transform: translateY(-1px);
}
.send-btn:disabled {
@ -315,19 +326,22 @@
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
background: rgba(4, 9, 14, 0.78);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(10px);
}
.image-modal-content, .contact-modal-content {
background: var(--bg-primary);
border-radius: 12px;
background: var(--panel-strong);
border-radius: 20px;
padding: 32px;
max-width: 90%;
position: relative;
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
.modal-close-btn {
@ -336,15 +350,16 @@
right: 16px;
width: 32px;
height: 32px;
background: var(--bg-secondary);
background: rgba(126, 200, 255, 0.08);
border: none;
border-radius: 8px;
border-radius: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all 0.2s;
border: 1px solid var(--border);
}
.modal-close-btn:hover {
@ -372,7 +387,8 @@
gap: 12px;
padding: 16px;
background: var(--bg-secondary);
border-radius: 8px;
border-radius: 16px;
border: 1px solid var(--border);
width: 100%;
max-width: 400px;
}
@ -383,10 +399,10 @@
.copy-btn {
padding: 12px 24px;
background: var(--primary);
background: linear-gradient(135deg, #8fd7ff, #63e6be);
border: none;
border-radius: 8px;
color: white;
border-radius: 14px;
color: #071018;
font-size: 14px;
font-weight: 500;
cursor: pointer;
@ -397,11 +413,11 @@
}
.copy-btn:hover {
background: var(--primary-dark);
transform: translateY(-1px);
}
.modal-image {
border-radius: 8px;
border-radius: 16px;
max-width: 100%;
display: block;
}
@ -425,11 +441,11 @@
}
.markdown code {
background: var(--bg-tertiary);
background: rgba(10, 20, 31, 0.82);
border: 1px solid var(--border);
border-radius: 4px;
border-radius: 10px;
padding: 2px 8px;
font-family: 'Courier New', monospace;
font-family: "IBM Plex Mono", monospace;
font-size: 13px;
}
@ -437,14 +453,14 @@
margin-top: 16px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
border-radius: 16px;
padding: 16px;
}
.chart {
width: 100%;
height: 400px;
border-radius: 4px;
border-radius: 14px;
}
/* Responsive */

View File

@ -11,7 +11,7 @@
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Page-Specific Styles -->
<style>
@ -26,11 +26,12 @@
.login-container {
width: 100%;
max-width: 420px;
background: var(--bg-primary);
background: var(--panel-strong);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 48px 40px;
box-shadow: var(--shadow-lg);
backdrop-filter: blur(18px);
}
.login-header {
@ -106,7 +107,7 @@
.send-code-btn {
padding: 12px 20px;
background: var(--bg-primary);
background: rgba(126, 200, 255, 0.08);
border: 1px solid var(--primary);
border-radius: var(--radius-md);
color: var(--primary);
@ -118,7 +119,7 @@
}
.send-code-btn:hover:not(:disabled) {
background: var(--primary-light);
background: rgba(126, 200, 255, 0.14);
}
.send-code-btn:disabled {
@ -129,18 +130,19 @@
.login-btn {
width: 100%;
padding: 14px;
background: var(--primary);
background: linear-gradient(135deg, #8fd7ff, #63e6be);
border: none;
border-radius: var(--radius-md);
color: white;
color: #071018;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 10px 24px rgba(49, 132, 189, 0.22);
}
.login-btn:hover:not(:disabled) {
background: var(--primary-dark);
transform: translateY(-1px);
}
.login-btn:disabled {

View File

@ -11,7 +11,7 @@
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Page-Specific Styles -->
<style>
@ -28,6 +28,12 @@
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
padding: 22px 24px;
background: var(--panel-strong);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.trading-title {
@ -41,13 +47,14 @@
.live-badge {
display: inline-block;
background: var(--error);
color: white;
padding: 4px 12px;
border-radius: 4px;
background: rgba(255, 111, 97, 0.16);
color: var(--error);
padding: 5px 12px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 1px;
border: 1px solid rgba(255, 111, 97, 0.22);
}
.header-actions {
@ -101,10 +108,10 @@
width: 18px;
left: 4px;
bottom: 3px;
background-color: white;
background-color: var(--text-primary);
transition: 0.3s;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.24);
}
input:checked + .slider {
@ -137,18 +144,19 @@
.refresh-btn {
padding: 10px 20px;
background: var(--bg-primary);
background: rgba(126, 200, 255, 0.08);
border: 1px solid var(--border);
border-radius: var(--radius-md);
border-radius: 14px;
color: var(--primary);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
backdrop-filter: blur(14px);
}
.refresh-btn:hover {
background: var(--primary-light);
background: rgba(126, 200, 255, 0.14);
border-color: var(--primary);
}
@ -162,6 +170,7 @@
display: flex;
align-items: center;
gap: 16px;
box-shadow: var(--shadow-soft);
}
.warning-banner.info {
@ -198,6 +207,8 @@
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 20px;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.account-label {
@ -262,6 +273,8 @@
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
table {

View File

@ -11,7 +11,7 @@
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Page-Specific Styles -->
<style>
@ -28,6 +28,12 @@
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
padding: 22px 24px;
background: var(--panel-strong);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.signals-title {
@ -54,18 +60,19 @@
.refresh-btn {
padding: 10px 20px;
background: var(--bg-primary);
background: rgba(126, 200, 255, 0.08);
border: 1px solid var(--border);
border-radius: var(--radius-md);
border-radius: 14px;
color: var(--primary);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
backdrop-filter: blur(14px);
}
.refresh-btn:hover {
background: var(--primary-light);
background: rgba(126, 200, 255, 0.14);
border-color: var(--primary);
}
@ -82,6 +89,7 @@
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 20px;
box-shadow: var(--shadow-soft);
}
.stat-label {
@ -125,6 +133,7 @@
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 16px;
box-shadow: var(--shadow-soft);
}
.grade-stat-header {
@ -186,6 +195,8 @@
border-radius: var(--radius-md);
padding: 20px;
transition: all 0.2s;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.signal-card:hover {
@ -215,9 +226,10 @@
.signal-type-badge {
font-size: 11px;
padding: 3px 8px;
background: var(--bg-tertiary);
background: rgba(128, 169, 202, 0.12);
color: var(--text-secondary);
border-radius: 4px;
border-radius: 999px;
border: 1px solid var(--border);
}
.signal-action-group {
@ -262,13 +274,13 @@
}
.signal-grade.B {
background: #E5E7EB;
color: #6B7280;
background: rgba(126, 200, 255, 0.12);
color: var(--primary);
}
.signal-grade.C {
background: #FED7AA;
color: #EA580C;
background: rgba(255, 184, 77, 0.16);
color: var(--warning);
}
.signal-grade.D {
@ -304,7 +316,7 @@
.confidence-fill {
height: 100%;
background: var(--primary);
background: linear-gradient(90deg, #7ec8ff, #63e6be);
transition: width 0.3s;
}
@ -332,7 +344,7 @@
.price-value {
font-size: 14px;
color: var(--text-primary);
font-family: 'Courier New', monospace;
font-family: "IBM Plex Mono", monospace;
}
/* Signal Details */
@ -358,10 +370,11 @@
/* Reason */
.signal-reason {
background: var(--bg-tertiary);
background: rgba(10, 20, 31, 0.72);
border-radius: var(--radius-sm);
padding: 12px;
margin-bottom: 12px;
border: 1px solid var(--border);
}
.reason-label {
@ -465,44 +478,6 @@
<button class="refresh-btn" @click="loadSignals">刷新</button>
</div>
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">加密货币信号</div>
<div class="stat-value">{{ stats.crypto.total }}</div>
<div class="stat-sub">24小时: {{ stats.crypto.recent_24h }}</div>
</div>
<div class="stat-card">
<div class="stat-label">美股信号</div>
<div class="stat-value">{{ stats.stock.total }}</div>
<div class="stat-sub">24小时: {{ stats.stock.recent_24h }}</div>
</div>
<div class="stat-card">
<div class="stat-label">总信号数</div>
<div class="stat-value">{{ stats.total }}</div>
</div>
<div class="stat-card">
<div class="stat-label">买入信号</div>
<div class="stat-value positive">{{ stats.crypto.buy + stats.stock.buy }}</div>
</div>
<div class="stat-card">
<div class="stat-label">卖出信号</div>
<div class="stat-value negative">{{ stats.crypto.sell + stats.stock.sell }}</div>
</div>
</div>
<!-- Grade Stats -->
<div class="grade-stats" v-if="Object.keys(stats.grades).length > 0">
<div class="grade-stat-card" v-for="(count, grade) in stats.grades" :key="grade">
<div class="grade-stat-header">
<span class="grade-stat-title">
<span class="signal-grade" :class="grade">{{ grade }}</span> 级信号
</span>
<span class="grade-stat-count">{{ count }}</span>
</div>
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<button class="tab" :class="{ active: currentTab === 'crypto' }" @click="switchTab('crypto')">

View File

@ -24,13 +24,13 @@
.status-page {
min-height: 100vh;
background: var(--bg-primary);
background: transparent;
padding: 20px;
}
.status-container {
max-width: 1400px;
min-width: 1200px;
min-width: 0;
margin: 0 auto;
}
@ -39,8 +39,9 @@
position: sticky;
top: 0;
z-index: 100;
background: var(--bg-primary);
background: linear-gradient(180deg, rgba(5, 11, 17, 0.94), rgba(5, 11, 17, 0.78));
padding-bottom: 10px;
backdrop-filter: blur(16px);
}
.status-header {
@ -48,15 +49,18 @@
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 10px 0 20px 0;
border-bottom: 1px solid var(--border);
background: var(--bg-primary);
padding: 18px 20px;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--panel-strong);
box-shadow: var(--shadow);
}
.status-title {
font-size: 24px;
font-weight: 300;
font-weight: 600;
color: var(--text-primary);
letter-spacing: -0.03em;
}
.status-title span {
@ -65,17 +69,20 @@
.refresh-btn {
padding: 8px 16px;
background: transparent;
border: 1px solid var(--accent);
color: var(--accent);
background: rgba(126, 200, 255, 0.08);
border: 1px solid var(--border);
color: var(--primary);
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
border-radius: 14px;
backdrop-filter: blur(14px);
}
.refresh-btn:hover {
background: var(--accent);
color: var(--bg-primary);
background: rgba(126, 200, 255, 0.14);
border-color: var(--primary);
color: var(--text-primary);
}
.last-update {
@ -96,8 +103,10 @@
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 4px;
border-radius: var(--radius-md);
padding: 16px;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.stat-label {
@ -108,8 +117,9 @@
.stat-value {
font-size: 24px;
font-weight: 300;
font-weight: 600;
color: var(--text-primary);
font-family: "IBM Plex Mono", monospace;
}
.stat-value.running {
@ -127,7 +137,7 @@
.section-title {
font-size: 18px;
font-weight: 300;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16px;
padding-bottom: 10px;
@ -143,14 +153,16 @@
.agent-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 4px;
border-radius: var(--radius-md);
padding: 20px;
transition: all 0.3s ease;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.agent-card:hover {
border-color: var(--accent);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: var(--line-strong);
box-shadow: var(--shadow);
}
.agent-card.status-running {
@ -249,7 +261,7 @@
background: rgba(255, 68, 68, 0.1);
color: #ff4444;
padding: 12px 16px;
border-radius: 4px;
border-radius: 14px;
margin-bottom: 16px;
border: 1px solid #ff4444;
}
@ -282,11 +294,12 @@
}
.symbol-tag {
background: var(--bg-primary);
background: rgba(126, 200, 255, 0.08);
padding: 2px 8px;
border-radius: 3px;
border-radius: 999px;
font-size: 11px;
color: var(--text-secondary);
border: 1px solid var(--border);
}
</style>
</head>

View File

@ -11,20 +11,21 @@
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Page-Specific Styles -->
<style>
.paper-badge {
display: inline-block;
background: var(--primary);
color: white;
padding: 4px 12px;
border-radius: var(--radius-sm);
background: linear-gradient(135deg, #8fd7ff, #63e6be);
color: #071018;
padding: 5px 12px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 1px;
margin-left: 12px;
box-shadow: 0 10px 24px rgba(49, 132, 189, 0.22);
}
.metrics-grid {
@ -47,6 +48,8 @@
border-radius: var(--radius-md);
padding: var(--space-lg);
transition: all 0.2s;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.metric-card:hover, .stat-card:hover {
@ -68,7 +71,7 @@
font-weight: 700;
color: var(--text-primary);
margin-bottom: var(--space-xs);
font-family: 'Inter', monospace;
font-family: "IBM Plex Mono", monospace;
}
.metric-sub {
@ -152,6 +155,7 @@
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-soft);
}
.price-list {
@ -167,7 +171,7 @@
padding: 12px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
border-radius: 14px;
}
.price-item .symbol {
@ -195,6 +199,7 @@
border-radius: var(--radius-md);
padding: 24px;
text-align: center;
box-shadow: var(--shadow-soft);
}
.core-metric-label {
@ -234,12 +239,13 @@
top: 100%;
right: 0;
margin-top: 8px;
background: var(--bg-primary);
background: var(--panel-strong);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
min-width: 200px;
z-index: 1000;
backdrop-filter: blur(18px);
}
.admin-menu-item {
@ -303,7 +309,7 @@
align-items: center;
padding: 2px 8px;
border-radius: 999px;
background: var(--bg-secondary);
background: rgba(126, 200, 255, 0.08);
border: 1px solid var(--border);
font-size: 11px;
color: var(--text-secondary);
@ -327,6 +333,7 @@
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 14px 16px;
box-shadow: var(--shadow-soft);
}
.platform-halt-card.halted {
@ -416,32 +423,6 @@
</div>
</div>
<div v-if="adminMode" class="platform-halts">
<div
v-for="(status, platform) in platformHalts"
:key="platform"
class="platform-halt-card"
:class="{ halted: status.halted }"
>
<div class="platform-halt-title">
{{ platform }} · {{ status.halted ? '已暂停' : '运行中' }}
</div>
<div class="platform-halt-text" v-if="status.halted">
{{ status.reason || '无暂停原因' }}
</div>
<div class="platform-halt-text" v-if="status.halted && status.halted_at">
暂停时间: {{ formatTime(status.halted_at) }}
</div>
<button
v-if="status.halted"
class="btn btn-secondary btn-small"
@click="resumePlatform(platform)"
>
恢复平台
</button>
</div>
</div>
<!-- Real-time Prices -->
<div v-if="Object.keys(latestPrices).length > 0" class="price-section">
<div class="stat-label" style="margin-bottom: 8px;">实时价格</div>