This commit is contained in:
aaron 2026-03-29 11:12:37 +08:00
parent 599a3498ba
commit e5fa1f2024
6 changed files with 1819 additions and 5159 deletions

View File

@ -8,145 +8,102 @@
<!-- Fonts --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Serif+Display&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css">
<style> <style>
/* ======================================== :root {
ADMIN PAGE - ADVANCED STYLING --primary: #0066FF;
======================================== */ --primary-light: #E8F0FF;
--bg-primary: #FFFFFF;
--bg-secondary: #F8FAFB;
--bg-tertiary: #F1F5F9;
--text-primary: #1E293B;
--text-secondary: #64748B;
--text-tertiary: #94A3B8;
--border: #E2E8F0;
--success: #10B981;
--success-light: #D1FAE5;
--error: #DC2626;
--error-light: #FEE2E2;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* === ATMOSPHERIC BACKGROUND === */
body { body {
background: var(--bg-primary); font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
position: relative; background: var(--bg-secondary);
overflow-x: hidden; color: var(--text-primary);
} line-height: 1.6;
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 10% 30%, rgba(102, 126, 234, 0.12) 0%, transparent 50%),
radial-gradient(circle at 90% 70%, rgba(118, 75, 162, 0.1) 0%, transparent 50%),
radial-gradient(circle at 50% 100%, rgba(0, 240, 255, 0.06) 0%, transparent 40%);
pointer-events: none;
z-index: 0;
animation: backgroundPulse 10s ease-in-out infinite;
}
@keyframes backgroundPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
} }
#app { #app {
position: relative;
z-index: 1;
}
.admin-page {
min-height: 100vh; min-height: 100vh;
background: transparent;
display: flex;
} }
/* === GLASSMORPHISM SIDEBAR === */ /* Admin Layout */
.admin-page {
display: flex;
min-height: 100vh;
}
/* Sidebar */
.admin-sidebar { .admin-sidebar {
width: 260px; width: 240px;
background: rgba(17, 24, 39, 0.9); background: var(--bg-primary);
backdrop-filter: blur(20px); border-right: 1px solid var(--border);
-webkit-backdrop-filter: blur(20px); padding: 24px 0;
border-right: 1px solid rgba(0, 240, 255, 0.2);
box-shadow: 4px 0 32px rgba(0, 0, 0, 0.5);
position: fixed; position: fixed;
height: 100vh; height: 100vh;
overflow-y: auto;
z-index: 100;
} }
.admin-logo { .admin-logo {
padding: 24px 24px 28px; padding: 0 24px 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); border-bottom: 1px solid var(--border);
margin-bottom: 20px; margin-bottom: 16px;
} }
.admin-logo h2 { .admin-logo h2 {
font-size: 20px; font-size: 18px;
font-weight: 300; font-weight: 600;
font-family: 'DM Serif Display', serif; color: var(--text-primary);
background: linear-gradient(135deg, #667EEA 0%, #764BA2 50%, #00F0FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 0.5px;
} }
.admin-nav { .admin-nav {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0;
} }
.admin-nav-item { .admin-nav-item {
padding: 16px 24px; padding: 12px 24px;
color: var(--text-secondary); color: var(--text-secondary);
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.2s;
border-left: 3px solid transparent; border-left: 3px solid transparent;
position: relative;
overflow: hidden;
font-weight: 500;
}
.admin-nav-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg, rgba(0, 240, 255, 0.1) 0%, transparent 100%);
opacity: 0;
transition: opacity 0.3s ease;
} }
.admin-nav-item:hover { .admin-nav-item:hover {
background: rgba(26, 31, 58, 0.6); background: var(--bg-secondary);
color: var(--text-primary); color: var(--text-primary);
border-left-color: rgba(0, 240, 255, 0.5);
}
.admin-nav-item:hover::before {
opacity: 1;
} }
.admin-nav-item.active { .admin-nav-item.active {
background: rgba(26, 31, 58, 0.9); background: var(--primary-light);
color: var(--accent); color: var(--primary);
border-left-color: var(--accent); border-left-color: var(--primary);
box-shadow: inset 0 0 20px rgba(0, 240, 255, 0.1); font-weight: 500;
} }
.admin-nav-item.active::before { /* Main Content */
opacity: 1;
}
/* === MAIN CONTENT === */
.admin-main { .admin-main {
margin-left: 260px; margin-left: 240px;
flex: 1; flex: 1;
padding: 24px; padding: 32px;
min-width: 800px;
}
.admin-container {
max-width: 1400px;
margin: 0 auto;
} }
.admin-header { .admin-header {
@ -154,61 +111,33 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 32px; margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
} }
.admin-title { .admin-title {
font-size: 32px; font-size: 28px;
font-weight: 300; font-weight: 700;
font-family: 'DM Serif Display', serif; color: var(--text-primary);
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 0.5px;
} }
.logout-btn { .logout-btn {
padding: 10px 20px; padding: 10px 20px;
background: rgba(26, 31, 58, 0.6); background: var(--bg-primary);
border: 1px solid rgba(255, 0, 64, 0.3); border: 1px solid var(--border);
border-radius: 12px; border-radius: 8px;
color: #ff0040; color: var(--text-secondary);
font-size: 13px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.2s;
position: relative;
overflow: hidden;
}
.logout-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 0, 64, 0.3);
transform: translate(-50%, -50%);
transition: width 0.4s ease, height 0.4s ease;
} }
.logout-btn:hover { .logout-btn:hover {
background: rgba(255, 0, 64, 0.15); background: var(--error-light);
border-color: rgba(255, 0, 64, 0.6); border-color: var(--error);
box-shadow: 0 0 16px rgba(255, 0, 64, 0.3); color: var(--error);
transform: translateY(-2px);
} }
.logout-btn:active::before { /* Stats Grid */
width: 200%;
height: 200%;
}
/* === STATS GRID === */
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
@ -217,38 +146,10 @@
} }
.stat-card { .stat-card {
background: rgba(26, 31, 58, 0.6); background: var(--bg-primary);
backdrop-filter: blur(10px); border: 1px solid var(--border);
-webkit-backdrop-filter: blur(10px); border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 24px; padding: 24px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, #667EEA 0%, #764BA2 50%, #00F0FF 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.stat-card:hover {
background: rgba(26, 31, 58, 0.9);
border-color: rgba(0, 240, 255, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 20px rgba(0, 240, 255, 0.2);
transform: translateY(-4px);
}
.stat-card:hover::before {
opacity: 1;
} }
.stat-label { .stat-label {
@ -260,179 +161,137 @@
} }
.stat-value { .stat-value {
font-size: 36px; font-size: 32px;
font-weight: 300; font-weight: 700;
font-family: 'JetBrains Mono', monospace; color: var(--primary);
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
} }
.section-title { /* Search Bar */
font-size: 20px;
font-weight: 300;
font-family: 'DM Serif Display', serif;
color: var(--text-primary);
margin-bottom: 24px;
}
/* === SEARCH BAR === */
.search-bar { .search-bar {
display: flex; display: flex;
gap: 16px; gap: 12px;
margin-bottom: 24px; margin-bottom: 24px;
} }
.search-input { .search-input {
flex: 1; flex: 1;
padding: 12px 18px; padding: 12px 16px;
background: rgba(26, 31, 58, 0.6); background: var(--bg-primary);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid var(--border);
border-radius: 12px; border-radius: 8px;
color: var(--text-primary);
font-size: 14px; font-size: 14px;
font-family: 'DM Sans', sans-serif; color: var(--text-primary);
transition: all 0.3s ease; transition: all 0.2s;
} }
.search-input:focus { .search-input:focus {
outline: none; outline: none;
background: rgba(26, 31, 58, 0.9); border-color: var(--primary);
border-color: rgba(0, 240, 255, 0.4);
box-shadow: 0 0 24px rgba(0, 240, 255, 0.2);
} }
.search-input::placeholder { .search-input::placeholder {
color: var(--text-tertiary); color: var(--text-tertiary);
opacity: 0.6;
} }
.search-btn { .search-btn {
padding: 12px 28px; padding: 12px 24px;
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%); background: var(--primary);
border: none; border: none;
border-radius: 12px; border-radius: 8px;
color: var(--bg-primary); color: white;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.2s;
box-shadow: 0 4px 16px rgba(0, 240, 255, 0.3);
} }
.search-btn:hover { .search-btn:hover {
box-shadow: 0 6px 24px rgba(0, 240, 255, 0.5); background: #0052CC;
transform: translateY(-2px);
} }
.search-btn:active { /* Table */
transform: translateY(0);
}
/* === USERS TABLE === */
.users-table { .users-table {
width: 100%; background: var(--bg-primary);
background: rgba(26, 31, 58, 0.6); border: 1px solid var(--border);
backdrop-filter: blur(10px); border-radius: 8px;
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
overflow: hidden; overflow: hidden;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
} }
.users-table table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
} }
.users-table th { th {
background: rgba(10, 14, 39, 0.6); background: var(--bg-tertiary);
padding: 16px 20px; padding: 14px 16px;
text-align: left; text-align: left;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: var(--text-secondary); color: var(--text-secondary);
border-bottom: 1px solid rgba(255, 255, 255, 0.1); border-bottom: 1px solid var(--border);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.users-table td { td {
padding: 16px 20px; padding: 14px 16px;
font-size: 14px; font-size: 14px;
color: var(--text-primary); color: var(--text-primary);
border-bottom: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid var(--border);
transition: all 0.2s ease;
} }
.users-table tr:last-child td { tr:last-child td {
border-bottom: none; border-bottom: none;
} }
.users-table tr { tr:hover {
transition: all 0.2s ease; background: var(--bg-secondary);
} }
.users-table tr:hover { /* Status Badge */
background: rgba(0, 240, 255, 0.05);
}
.users-table tr:hover td {
color: var(--text-primary);
}
/* === STATUS BADGES === */
.status-badge { .status-badge {
display: inline-block; display: inline-block;
padding: 6px 16px; padding: 4px 12px;
border-radius: 12px; border-radius: 12px;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
letter-spacing: 0.5px;
} }
.status-active { .status-active {
background: linear-gradient(135deg, rgba(0, 200, 81, 0.2) 0%, rgba(0, 230, 118, 0.2) 100%); background: var(--success-light);
color: #00C851; color: var(--success);
border: 1px solid rgba(0, 200, 81, 0.4);
box-shadow: 0 0 12px rgba(0, 200, 81, 0.2);
} }
.status-inactive { .status-inactive {
background: linear-gradient(135deg, rgba(255, 0, 64, 0.2) 0%, rgba(255, 107, 107, 0.2) 100%); background: var(--error-light);
color: #FF0040; color: var(--error);
border: 1px solid rgba(255, 0, 64, 0.4);
box-shadow: 0 0 12px rgba(255, 0, 64, 0.2);
} }
/* === PAGINATION === */ /* Pagination */
.pagination { .pagination {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 16px; gap: 12px;
margin-top: 24px; margin-top: 24px;
} }
.pagination button { .pagination button {
padding: 10px 20px; padding: 8px 16px;
background: rgba(26, 31, 58, 0.6); background: var(--bg-primary);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid var(--border);
border-radius: 12px; border-radius: 8px;
color: var(--text-primary); color: var(--text-primary);
font-size: 14px; font-size: 14px;
font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.2s;
} }
.pagination button:hover:not(:disabled) { .pagination button:hover:not(:disabled) {
background: rgba(0, 240, 255, 0.15); background: var(--primary-light);
border-color: rgba(0, 240, 255, 0.4); border-color: var(--primary);
box-shadow: 0 0 16px rgba(0, 240, 255, 0.2); color: var(--primary);
transform: translateY(-2px);
} }
.pagination button:disabled { .pagination button:disabled {
@ -440,131 +299,92 @@
cursor: not-allowed; cursor: not-allowed;
} }
.pagination .page-info { .page-info {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 14px; font-size: 14px;
font-weight: 500;
} }
/* === LOGIN OVERLAY === */ /* Login Overlay */
.login-overlay { .login-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; width: 100%;
bottom: 0; height: 100%;
background: rgba(10, 14, 39, 0.95); background: var(--bg-secondary);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1000; z-index: 1000;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
} }
.login-box { .login-box {
width: 100%; width: 100%;
max-width: 440px; max-width: 400px;
background: var(--bg-primary);
border-radius: 12px;
padding: 48px; padding: 48px;
background: rgba(26, 31, 58, 0.95); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.6), 0 0 40px rgba(0, 240, 255, 0.2);
animation: scaleIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.9) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
} }
.login-box h2 { .login-box h2 {
font-size: 32px; font-size: 28px;
font-weight: 300; font-weight: 700;
font-family: 'DM Serif Display', serif; color: var(--text-primary);
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 32px; margin-bottom: 32px;
text-align: center; text-align: center;
} }
.login-box input { .login-box input {
width: 100%; width: 100%;
padding: 14px 18px; padding: 12px 16px;
background: rgba(10, 14, 39, 0.6); background: var(--bg-secondary);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid var(--border);
border-radius: 12px; border-radius: 8px;
color: var(--text-primary);
font-size: 14px; font-size: 14px;
font-family: 'DM Sans', sans-serif; color: var(--text-primary);
margin-bottom: 20px; margin-bottom: 16px;
transition: all 0.3s ease;
} }
.login-box input:focus { .login-box input:focus {
outline: none; outline: none;
background: rgba(10, 14, 39, 0.9); background: var(--bg-primary);
border-color: rgba(0, 240, 255, 0.4); border-color: var(--primary);
box-shadow: 0 0 24px rgba(0, 240, 255, 0.2);
} }
.login-box input::placeholder { .login-box input::placeholder {
color: var(--text-tertiary); color: var(--text-tertiary);
opacity: 0.6;
} }
.login-box button { .login-box button {
width: 100%; width: 100%;
padding: 16px; padding: 14px;
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%); background: var(--primary);
border: none; border: none;
border-radius: 12px; border-radius: 8px;
color: var(--bg-primary); color: white;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.2s;
box-shadow: 0 4px 16px rgba(0, 240, 255, 0.3);
} }
.login-box button:hover { .login-box button:hover {
box-shadow: 0 6px 24px rgba(0, 240, 255, 0.5); background: #0052CC;
transform: translateY(-2px);
}
.login-box button:active {
transform: translateY(0);
} }
.error-message { .error-message {
padding: 14px; padding: 12px 16px;
background: rgba(255, 0, 64, 0.15); background: var(--error-light);
border: 1px solid rgba(255, 0, 64, 0.4); border: 1px solid var(--error);
border-radius: 12px; border-radius: 8px;
color: #ff0040; color: var(--error);
font-size: 13px; font-size: 13px;
text-align: center; text-align: center;
margin-bottom: 20px; margin-bottom: 16px;
box-shadow: 0 0 16px rgba(255, 0, 64, 0.2);
} }
/* === RESPONSIVE === */ /* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.admin-sidebar { .admin-sidebar {
width: 200px; width: 200px;
@ -572,27 +392,18 @@
.admin-main { .admin-main {
margin-left: 200px; margin-left: 200px;
min-width: 600px; padding: 20px;
padding: 16px;
} }
.admin-title { .admin-title {
font-size: 24px; font-size: 24px;
} }
.stats-grid {
grid-template-columns: 1fr;
}
.stat-value {
font-size: 28px;
}
} }
</style> </style>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<!-- 登录遮罩 --> <!-- Login Overlay -->
<div v-if="!authenticated" class="login-overlay"> <div v-if="!authenticated" class="login-overlay">
<div class="login-box"> <div class="login-box">
<h2>后台管理</h2> <h2>后台管理</h2>
@ -609,32 +420,35 @@
</div> </div>
</div> </div>
<!-- 管理页面 --> <!-- Admin Page -->
<div v-if="authenticated" class="admin-page"> <div v-if="authenticated" class="admin-page">
<!-- 侧边栏 --> <!-- Sidebar -->
<div class="admin-sidebar"> <div class="admin-sidebar">
<div class="admin-logo"> <div class="admin-logo">
<h2>Tradus 管理后台</h2> <h2>Tradus 管理后台</h2>
</div> </div>
<ul class="admin-nav"> <ul class="admin-nav">
<li class="admin-nav-item" :class="{active: currentView === 'dashboard'}" @click="currentView = 'dashboard'"> <li class="admin-nav-item"
:class="{active: currentView === 'dashboard'}"
@click="currentView = 'dashboard'">
数据统计 数据统计
</li> </li>
<li class="admin-nav-item" :class="{active: currentView === 'users'}" @click="currentView = 'users'"> <li class="admin-nav-item"
:class="{active: currentView === 'users'}"
@click="currentView = 'users'">
用户管理 用户管理
</li> </li>
</ul> </ul>
</div> </div>
<!-- 主内容区 --> <!-- Main Content -->
<div class="admin-main"> <div class="admin-main">
<div class="admin-container">
<div class="admin-header"> <div class="admin-header">
<h1 class="admin-title">{{ currentViewTitle }}</h1> <h1 class="admin-title">{{ currentViewTitle }}</h1>
<button class="logout-btn" @click="logout">退出</button> <button class="logout-btn" @click="logout">退出</button>
</div> </div>
<!-- 数据统计视图 --> <!-- Dashboard View -->
<div v-if="currentView === 'dashboard'"> <div v-if="currentView === 'dashboard'">
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
@ -656,9 +470,8 @@
</div> </div>
</div> </div>
<!-- 用户管理视图 --> <!-- Users View -->
<div v-if="currentView === 'users'"> <div v-if="currentView === 'users'">
<h3 class="section-title">用户列表</h3>
<div class="search-bar"> <div class="search-bar">
<input <input
type="text" type="text"
@ -706,7 +519,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script> <script>
@ -719,7 +531,7 @@
password: '', password: '',
errorMessage: '', errorMessage: '',
adminPassword: '', adminPassword: '',
currentView: 'users', // 当前视图dashboard 或 users currentView: 'users',
stats: { stats: {
total_users: 0, total_users: 0,
active_users: 0, active_users: 0,
@ -744,7 +556,6 @@
} }
}, },
mounted() { mounted() {
// 检查是否已登录
const savedPassword = sessionStorage.getItem('admin_password'); const savedPassword = sessionStorage.getItem('admin_password');
if (savedPassword) { if (savedPassword) {
this.adminPassword = savedPassword; this.adminPassword = savedPassword;
@ -851,11 +662,8 @@
formatDate(dateString) { formatDate(dateString) {
if (!dateString) return '-'; if (!dateString) return '-';
// 直接解析 ISO 格式的时间字符串,不做时区转换 const date = new Date(dateString + 'Z');
// 假设服务器返回的是 UTC 时间,我们将其视为本地时间显示
const date = new Date(dateString + 'Z'); // 添加 Z 表示 UTC
// 获取各个时间部分
const year = date.getFullYear(); const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0');

File diff suppressed because it is too large Load Diff

View File

@ -3,182 +3,115 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - TradusAI 金融智能体</title> <title>登录 - Tradus AI</title>
<!-- Fonts --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Serif+Display&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css">
<style> <style>
/* ======================================== :root {
LOGIN PAGE - ADVANCED STYLING --primary: #0066FF;
======================================== */ --primary-light: #E8F0FF;
--bg-primary: #FFFFFF;
--bg-secondary: #F8FAFB;
--bg-tertiary: #F1F5F9;
--text-primary: #1E293B;
--text-secondary: #64748B;
--text-tertiary: #94A3B8;
--border: #E2E8F0;
--error: #DC2626;
--error-light: #FEE2E2;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* === ATMOSPHERIC BACKGROUND === */
body { body {
background: var(--bg-primary); font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
position: relative; background: var(--bg-secondary);
overflow: hidden; color: var(--text-primary);
} line-height: 1.6;
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 20% 30%, rgba(102, 126, 234, 0.18) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(118, 75, 162, 0.15) 0%, transparent 50%),
radial-gradient(circle at 50% 100%, rgba(0, 240, 255, 0.1) 0%, transparent 40%);
pointer-events: none;
z-index: 0;
animation: loginBackgroundPulse 8s ease-in-out infinite;
}
@keyframes loginBackgroundPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
#app {
position: relative;
z-index: 1;
}
.login-page {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: transparent;
padding: 20px; padding: 20px;
} }
/* === GLASSMORPHISM LOGIN CONTAINER === */
.login-container { .login-container {
width: 100%; width: 100%;
max-width: 480px; max-width: 420px;
padding: 56px 48px; background: var(--bg-primary);
background: rgba(26, 31, 58, 0.95); border-radius: 12px;
backdrop-filter: blur(20px); padding: 48px 40px;
-webkit-backdrop-filter: blur(20px); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
box-shadow:
0 16px 64px rgba(0, 0, 0, 0.6),
0 0 40px rgba(0, 240, 255, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
animation: loginSlideIn 0.6s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
} }
.login-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, #667EEA 0%, #764BA2 50%, #00F0FF 100%);
opacity: 0.8;
}
@keyframes loginSlideIn {
from {
opacity: 0;
transform: translateY(40px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* === HEADER === */
.login-header { .login-header {
text-align: center; text-align: center;
margin-bottom: 48px; margin-bottom: 40px;
} }
.login-logo { .login-logo {
display: flex; margin-bottom: 24px;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 20px;
} }
.login-logo svg { .login-logo svg {
color: var(--accent); width: 48px;
filter: drop-shadow(0 0 12px rgba(0, 240, 255, 0.6)); height: 48px;
animation: logoFloat 4s ease-in-out infinite; color: var(--primary);
}
@keyframes logoFloat {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-8px) rotate(5deg); }
} }
.login-title { .login-title {
font-size: 32px; font-size: 28px;
font-weight: 300; font-weight: 700;
font-family: 'DM Serif Display', serif; color: var(--text-primary);
background: linear-gradient(135deg, #667EEA 0%, #764BA2 50%, #00F0FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 1px;
line-height: 1.4;
} }
/* === FORM === */
.login-form { .login-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 20px;
} }
.form-group { .form-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 8px;
} }
.form-label { .form-label {
font-size: 13px; font-size: 13px;
color: var(--text-secondary);
letter-spacing: 0.5px;
font-weight: 500; font-weight: 500;
text-transform: uppercase; color: var(--text-secondary);
} }
.form-input { .form-input {
width: 100%; width: 100%;
padding: 14px 18px; padding: 12px 16px;
background: rgba(10, 14, 39, 0.6); background: var(--bg-secondary);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid var(--border);
border-radius: 12px; border-radius: 8px;
color: var(--text-primary);
font-size: 15px; font-size: 15px;
font-family: 'DM Sans', sans-serif; font-family: inherit;
transition: all 0.3s ease; color: var(--text-primary);
transition: all 0.2s;
} }
.form-input:focus { .form-input:focus {
outline: none; outline: none;
background: rgba(10, 14, 39, 0.9); background: var(--bg-primary);
border-color: rgba(0, 240, 255, 0.5); border-color: var(--primary);
box-shadow: 0 0 24px rgba(0, 240, 255, 0.25); box-shadow: 0 0 0 3px var(--primary-light);
} }
.form-input::placeholder { .form-input::placeholder {
color: var(--text-tertiary); color: var(--text-tertiary);
opacity: 0.6;
} }
.code-input-group { .code-input-group {
@ -191,43 +124,20 @@
} }
.send-code-btn { .send-code-btn {
padding: 14px 24px; padding: 12px 20px;
background: rgba(26, 31, 58, 0.6); background: var(--bg-primary);
border: 1px solid rgba(0, 240, 255, 0.3); border: 1px solid var(--primary);
border-radius: 12px; border-radius: 8px;
color: var(--accent); color: var(--primary);
font-size: 13px; font-size: 14px;
font-weight: 600; font-weight: 500;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
transition: all 0.3s ease; transition: all 0.2s;
position: relative;
overflow: hidden;
}
.send-code-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(0, 240, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.4s ease, height 0.4s ease;
} }
.send-code-btn:hover:not(:disabled) { .send-code-btn:hover:not(:disabled) {
background: rgba(0, 240, 255, 0.15); background: var(--primary-light);
border-color: rgba(0, 240, 255, 0.6);
box-shadow: 0 0 20px rgba(0, 240, 255, 0.3);
transform: translateY(-2px);
}
.send-code-btn:active:not(:disabled)::before {
width: 200%;
height: 200%;
} }
.send-code-btn:disabled { .send-code-btn:disabled {
@ -237,83 +147,50 @@
.login-btn { .login-btn {
width: 100%; width: 100%;
padding: 16px; padding: 14px;
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%); background: var(--primary);
border: none; border: none;
border-radius: 12px; border-radius: 8px;
color: var(--bg-primary); color: white;
font-size: 16px; font-size: 15px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.2s;
box-shadow: 0 4px 20px rgba(0, 240, 255, 0.4);
position: relative;
overflow: hidden;
}
.login-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%);
transition: left 0.5s ease;
} }
.login-btn:hover:not(:disabled) { .login-btn:hover:not(:disabled) {
box-shadow: 0 6px 32px rgba(0, 240, 255, 0.6); background: #0052CC;
transform: translateY(-2px);
}
.login-btn:hover:not(:disabled)::before {
left: 100%;
}
.login-btn:active:not(:disabled) {
transform: translateY(0);
} }
.login-btn:disabled { .login-btn:disabled {
opacity: 0.5; opacity: 0.4;
cursor: not-allowed; cursor: not-allowed;
} }
.error-message { .error-message {
padding: 14px 18px; padding: 12px 16px;
background: rgba(255, 0, 64, 0.15); background: var(--error-light);
border: 1px solid rgba(255, 0, 64, 0.4); border: 1px solid var(--error);
border-radius: 12px; border-radius: 8px;
color: #ff0040; color: var(--error);
font-size: 13px; font-size: 13px;
text-align: center; text-align: center;
box-shadow: 0 0 20px rgba(255, 0, 64, 0.2);
animation: errorShake 0.4s ease-out;
}
@keyframes errorShake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-8px); }
75% { transform: translateX(8px); }
} }
.login-footer { .login-footer {
margin-top: 32px; margin-top: 24px;
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
color: var(--text-tertiary); color: var(--text-tertiary);
opacity: 0.7;
} }
/* === RESPONSIVE === */ @media (max-width: 480px) {
@media (max-width: 768px) {
.login-container { .login-container {
padding: 40px 32px; padding: 32px 24px;
} }
.login-title { .login-title {
font-size: 26px; font-size: 24px;
} }
.code-input-group { .code-input-group {
@ -324,26 +201,6 @@
width: 100%; width: 100%;
} }
} }
@media (max-width: 480px) {
.login-container {
padding: 32px 24px;
}
.login-title {
font-size: 22px;
}
.form-input {
padding: 12px 16px;
font-size: 14px;
}
.login-btn {
padding: 14px;
font-size: 15px;
}
}
</style> </style>
</head> </head>
<body> <body>
@ -351,7 +208,7 @@
<div class="login-container"> <div class="login-container">
<div class="login-header"> <div class="login-header">
<div class="login-logo"> <div class="login-logo">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"> <svg viewBox="0 0 24 24" fill="none">
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/> <path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/>
</svg> </svg>
</div> </div>
@ -500,9 +357,7 @@
const data = await response.json(); const data = await response.json();
if (data.success && data.token) { if (data.success && data.token) {
// 保存token
localStorage.setItem('token', data.token); localStorage.setItem('token', data.token);
// 跳转到主页
window.location.href = '/app'; window.location.href = '/app';
} else { } else {
this.errorMessage = data.message || '登录失败'; this.errorMessage = data.message || '登录失败';

File diff suppressed because it is too large Load Diff

View File

@ -2,130 +2,104 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>交易信号 - Stock Agent</title> <title>交易信号 - Tradus</title>
<!-- Fonts --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Serif+Display&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css">
<style> <style>
/* ===== 页面基础 ===== */ :root {
html, body { --primary: #0066FF;
overflow-x: hidden; --primary-light: #E8F0FF;
max-width: 100vw; --bg-primary: #FFFFFF;
background: linear-gradient(180deg, #0A0E27 0%, #000000 100%); --bg-secondary: #F8FAFB;
--bg-tertiary: #F1F5F9;
--text-primary: #1E293B;
--text-secondary: #64748B;
--text-tertiary: #94A3B8;
--border: #E2E8F0;
--success: #10B981;
--success-light: #D1FAE5;
--error: #EF4444;
--error-light: #FEE2E2;
--warning: #F59E0B;
--warning-light: #FEF3C7;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-secondary);
color: var(--text-primary);
line-height: 1.6;
} }
#app { #app {
height: auto; min-height: 100vh;
display: block;
align-items: initial;
justify-content: initial;
padding: 0;
overflow-x: hidden;
} }
.signals-page { .signals-page {
min-height: 100vh; max-width: 1400px;
background: transparent;
padding: 0;
position: relative;
}
/* 背景装饰 */
.signals-page::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
radial-gradient(circle at 30% 30%, rgba(102, 126, 234, 0.12) 0%, transparent 50%),
radial-gradient(circle at 70% 70%, rgba(0, 240, 255, 0.08) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.signals-container {
max-width: 1600px;
margin: 0 auto; margin: 0 auto;
padding: 24px; padding: 32px 24px;
position: relative;
z-index: 1;
}
/* ===== 顶部导航 ===== */
.sticky-header {
position: sticky;
top: 0;
z-index: 100;
background: rgba(10, 14, 39, 0.8);
backdrop-filter: blur(20px);
padding: 16px 24px;
margin: -24px -24px 24px -24px;
border-bottom: 1px solid rgba(0, 240, 255, 0.2);
} }
/* Header */
.signals-header { .signals-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 16px; margin-bottom: 32px;
} }
.signals-title { .signals-title {
font-size: 28px; font-size: 28px;
font-weight: 500; font-weight: 700;
font-family: 'DM Serif Display', serif; color: var(--text-primary);
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
} }
.signals-title::before { .live-dot {
content: '';
width: 8px; width: 8px;
height: 8px; height: 8px;
background: #00F0FF; background: var(--success);
border-radius: 50%; border-radius: 50%;
box-shadow: 0 0 12px #00F0FF, 0 0 24px #00F0FF;
animation: pulse 2s ease-in-out infinite; animation: pulse 2s ease-in-out infinite;
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); } 0%, 100% { opacity: 1; }
50% { opacity: 0.5; transform: scale(1.2); } 50% { opacity: 0.5; }
} }
.refresh-btn { .refresh-btn {
padding: 10px 20px; padding: 10px 20px;
background: rgba(0, 240, 255, 0.1); background: var(--bg-primary);
border: 1px solid rgba(0, 240, 255, 0.3); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
color: #00F0FF; color: var(--primary);
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.2s;
font-family: 'DM Sans', sans-serif;
} }
.refresh-btn:hover { .refresh-btn:hover {
background: rgba(0, 240, 255, 0.2); background: var(--primary-light);
border-color: #00F0FF; border-color: var(--primary);
box-shadow: 0 0 20px rgba(0, 240, 255, 0.4);
transform: translateY(-2px);
} }
/* ===== 统计卡片网格 ===== */ /* Stats Grid */
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
@ -134,109 +108,102 @@
} }
.stat-card { .stat-card {
position: relative; background: var(--bg-primary);
background: rgba(26, 31, 58, 0.6); border: 1px solid var(--border);
backdrop-filter: blur(10px); border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 20px; padding: 20px;
overflow: hidden;
transition: all 0.3s ease;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, #667EEA 0%, #764BA2 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
border-color: rgba(0, 240, 255, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 20px rgba(0, 240, 255, 0.2);
}
.stat-card:hover::before {
opacity: 1;
} }
.stat-label { .stat-label {
font-size: 12px; font-size: 12px;
color: var(--text-secondary); color: var(--text-secondary);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 0.5px;
margin-bottom: 8px; margin-bottom: 8px;
font-weight: 500;
} }
.stat-value { .stat-value {
font-size: 28px; font-size: 28px;
font-weight: 700; font-weight: 700;
font-family: 'JetBrains Mono', monospace; color: var(--primary);
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%); }
-webkit-background-clip: text;
-webkit-text-fill-color: transparent; .stat-sub {
background-clip: text; font-size: 12px;
color: var(--text-tertiary);
margin-top: 4px;
} }
.stat-value.positive { .stat-value.positive {
background: linear-gradient(135deg, #00C851 0%, #00E676 100%); color: var(--success);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
} }
.stat-value.negative { .stat-value.negative {
background: linear-gradient(135deg, #FF4444 0%, #FF6B6B 100%); color: var(--error);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
} }
/* ===== 标签页导航 ===== */ /* Grade Stats */
.grade-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.grade-stat-card {
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
}
.grade-stat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.grade-stat-title {
font-size: 14px;
color: var(--text-primary);
}
.grade-stat-count {
font-size: 20px;
font-weight: 700;
color: var(--primary);
}
/* Tabs */
.tabs { .tabs {
display: flex; display: flex;
gap: 0; border-bottom: 1px solid var(--border);
margin-bottom: 24px; margin-bottom: 24px;
background: rgba(26, 31, 58, 0.4);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
} }
.tab { .tab {
flex: 1;
padding: 14px 24px; padding: 14px 24px;
background: transparent; background: transparent;
border: none; border: none;
border-radius: 8px; border-bottom: 2px solid transparent;
color: var(--text-secondary); color: var(--text-secondary);
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.2s;
position: relative;
} }
.tab:hover { .tab:hover {
color: var(--text-primary); color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
} }
.tab.active { .tab.active {
background: linear-gradient(135deg, rgba(0, 240, 255, 0.15) 0%, rgba(102, 126, 234, 0.15) 100%); color: var(--primary);
color: #00F0FF; border-bottom-color: var(--primary);
box-shadow: 0 0 20px rgba(0, 240, 255, 0.2);
} }
/* 信号卡片网格 */ /* Signals Grid */
.signals-grid { .signals-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
@ -244,16 +211,16 @@
} }
.signal-card { .signal-card {
background: var(--bg-secondary); background: var(--bg-primary);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 4px; border-radius: 8px;
padding: 20px; padding: 20px;
transition: all 0.2s; transition: all 0.2s;
} }
.signal-card:hover { .signal-card:hover {
border-color: var(--accent); border-color: var(--primary);
box-shadow: 0 4px 12px rgba(0, 200, 150, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
} }
.signal-header { .signal-header {
@ -270,8 +237,8 @@
} }
.signal-symbol { .signal-symbol {
font-size: 20px; font-size: 18px;
font-weight: 500; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
} }
@ -280,7 +247,7 @@
padding: 3px 8px; padding: 3px 8px;
background: var(--bg-tertiary); background: var(--bg-tertiary);
color: var(--text-secondary); color: var(--text-secondary);
border-radius: 2px; border-radius: 4px;
} }
.signal-action-group { .signal-action-group {
@ -291,55 +258,55 @@
.signal-action { .signal-action {
padding: 6px 16px; padding: 6px 16px;
border-radius: 2px; border-radius: 4px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 600;
} }
.signal-action.buy { .signal-action.buy {
background: rgba(0, 255, 65, 0.1); background: var(--success-light);
color: #00ff41; color: var(--success);
} }
.signal-action.sell { .signal-action.sell {
background: rgba(255, 68, 68, 0.1); background: var(--error-light);
color: #ff4444; color: var(--error);
} }
.signal-action.hold { .signal-action.hold {
background: rgba(255, 255, 255, 0.1); background: var(--bg-tertiary);
color: var(--text-secondary); color: var(--text-secondary);
} }
.signal-grade { .signal-grade {
display: inline-block; display: inline-block;
padding: 4px 10px; padding: 4px 10px;
border-radius: 2px; border-radius: 4px;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 600;
} }
.signal-grade.A { .signal-grade.A {
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 179, 0, 0.2)); background: var(--warning-light);
color: gold; color: var(--warning);
} }
.signal-grade.B { .signal-grade.B {
background: linear-gradient(135deg, rgba(192, 192, 192, 0.2), rgba(160, 160, 160, 0.2)); background: #E5E7EB;
color: silver; color: #6B7280;
} }
.signal-grade.C { .signal-grade.C {
background: linear-gradient(135deg, rgba(205, 127, 50, 0.2), rgba(160, 82, 45, 0.2)); background: #FED7AA;
color: #cd7f32; color: #EA580C;
} }
.signal-grade.D { .signal-grade.D {
background: rgba(255, 255, 255, 0.1); background: var(--error-light);
color: var(--text-secondary); color: var(--error);
} }
/* 置信度条 */ /* Confidence */
.confidence-section { .confidence-section {
margin-bottom: 16px; margin-bottom: 16px;
} }
@ -354,8 +321,8 @@
} }
.confidence-value { .confidence-value {
color: var(--accent); color: var(--primary);
font-weight: 500; font-weight: 600;
} }
.confidence-bar { .confidence-bar {
@ -367,11 +334,11 @@
.confidence-fill { .confidence-fill {
height: 100%; height: 100%;
background: linear-gradient(90deg, var(--accent) 0%, #00ff41 100%); background: var(--primary);
transition: width 0.3s; transition: width 0.3s;
} }
/* 价格信息 */ /* Price Section */
.price-section { .price-section {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@ -395,10 +362,10 @@
.price-value { .price-value {
font-size: 14px; font-size: 14px;
color: var(--text-primary); color: var(--text-primary);
font-family: monospace; font-family: 'Courier New', monospace;
} }
/* 信号详情 */ /* Signal Details */
.signal-details { .signal-details {
margin-bottom: 12px; margin-bottom: 12px;
} }
@ -419,7 +386,7 @@
flex: 1; flex: 1;
} }
/* 分析理由 */ /* Reason */
.signal-reason { .signal-reason {
background: var(--bg-tertiary); background: var(--bg-tertiary);
border-radius: 4px; border-radius: 4px;
@ -439,13 +406,13 @@
line-height: 1.5; line-height: 1.5;
} }
/* 时间戳 */ /* Time */
.signal-time { .signal-time {
font-size: 11px; font-size: 11px;
color: var(--text-secondary); color: var(--text-tertiary);
} }
/* 空状态 */ /* Empty State */
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 60px 20px; padding: 60px 20px;
@ -456,32 +423,20 @@
width: 48px; width: 48px;
height: 48px; height: 48px;
margin-bottom: 16px; margin-bottom: 16px;
opacity: 0.5; opacity: 0.3;
} }
/* 加载状态 */ /* Loading */
.loading { .loading {
text-align: center; text-align: center;
padding: 40px; padding: 40px;
color: var(--text-secondary); color: var(--text-secondary);
} }
/* 响应式设计 */ /* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.signals-page { .signals-page {
padding: 10px; padding: 20px 16px;
overflow-x: hidden;
}
.signals-container {
min-width: auto;
max-width: 100%;
overflow-x: hidden;
}
/* 取消顶部固定 */
.sticky-header {
position: static;
} }
.signals-header { .signals-header {
@ -491,12 +446,7 @@
} }
.signals-title { .signals-title {
font-size: 18px; font-size: 22px;
}
.signals-title span {
display: block;
font-size: 14px;
} }
.stats-grid { .stats-grid {
@ -505,18 +455,15 @@
} }
.stat-card { .stat-card {
padding: 12px; padding: 14px;
min-width: 0;
} }
.stat-value { .stat-value {
font-size: 16px; font-size: 22px;
word-break: break-all;
} }
.tabs { .tabs {
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch;
} }
.tab { .tab {
@ -534,91 +481,31 @@
gap: 8px; gap: 8px;
} }
} }
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.signals-title {
font-size: 16px;
}
.stat-value {
font-size: 16px;
}
}
/* 等级统计 */
.grade-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.grade-stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 4px;
padding: 16px;
}
.grade-stat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.grade-stat-title {
font-size: 14px;
color: var(--text-primary);
}
.grade-stat-count {
font-size: 20px;
font-weight: 300;
color: var(--accent);
}
.grade-stat-details {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 12px;
}
.grade-stat-row {
display: flex;
justify-content: space-between;
color: var(--text-secondary);
}
</style> </style>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<div class="signals-page"> <div class="signals-page">
<div class="signals-container"> <!-- Header -->
<!-- 固定顶部区域 -->
<div class="sticky-header">
<!-- 头部 -->
<div class="signals-header"> <div class="signals-header">
<h1 class="signals-title">交易信号中心 <span>| Trading Signals</span></h1> <h1 class="signals-title">
交易信号中心
<span class="live-dot"></span>
</h1>
<button class="refresh-btn" @click="loadSignals">刷新</button> <button class="refresh-btn" @click="loadSignals">刷新</button>
</div> </div>
<!-- 统计卡片 --> <!-- Stats Grid -->
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
<div class="stat-label">加密货币信号</div> <div class="stat-label">加密货币信号</div>
<div class="stat-value">{{ stats.crypto.total }}</div> <div class="stat-value">{{ stats.crypto.total }}</div>
<div class="stat-label" style="margin-top: 6px;">最近24小时: {{ stats.crypto.recent_24h }}</div> <div class="stat-sub">24小时: {{ stats.crypto.recent_24h }}</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-label">美股信号</div> <div class="stat-label">美股信号</div>
<div class="stat-value">{{ stats.stock.total }}</div> <div class="stat-value">{{ stats.stock.total }}</div>
<div class="stat-label" style="margin-top: 6px;">最近24小时: {{ stats.stock.recent_24h }}</div> <div class="stat-sub">24小时: {{ stats.stock.recent_24h }}</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-label">总信号数</div> <div class="stat-label">总信号数</div>
@ -634,7 +521,7 @@
</div> </div>
</div> </div>
<!-- 等级统计 --> <!-- Grade Stats -->
<div class="grade-stats" v-if="Object.keys(stats.grades).length > 0"> <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-card" v-for="(count, grade) in stats.grades" :key="grade">
<div class="grade-stat-header"> <div class="grade-stat-header">
@ -645,9 +532,8 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- 标签页 --> <!-- Tabs -->
<div class="tabs"> <div class="tabs">
<button class="tab" :class="{ active: currentTab === 'crypto' }" @click="switchTab('crypto')"> <button class="tab" :class="{ active: currentTab === 'crypto' }" @click="switchTab('crypto')">
加密货币 ({{ cryptoSignals.length }}) 加密货币 ({{ cryptoSignals.length }})
@ -660,7 +546,7 @@
</button> </button>
</div> </div>
<!-- 信号列表 --> <!-- Signals List -->
<div v-if="loading" class="loading">加载中...</div> <div v-if="loading" class="loading">加载中...</div>
<div v-else-if="currentSignals.length === 0" class="empty-state"> <div v-else-if="currentSignals.length === 0" class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
@ -670,8 +556,8 @@
<p>暂无信号</p> <p>暂无信号</p>
</div> </div>
<div v-else class="signals-grid"> <div v-else class="signals-grid">
<div v-for="signal in currentSignals" :key="signal.id" class="signal-card" :class="signal.action"> <div v-for="signal in currentSignals" :key="signal.id" class="signal-card">
<!-- 信号头部 --> <!-- Header -->
<div class="signal-header"> <div class="signal-header">
<div class="signal-symbol-group"> <div class="signal-symbol-group">
<span class="signal-symbol">{{ signal.symbol }}</span> <span class="signal-symbol">{{ signal.symbol }}</span>
@ -685,7 +571,7 @@
</div> </div>
</div> </div>
<!-- 置信度 --> <!-- Confidence -->
<div class="confidence-section"> <div class="confidence-section">
<div class="confidence-label"> <div class="confidence-label">
<span>置信度</span> <span>置信度</span>
@ -696,7 +582,7 @@
</div> </div>
</div> </div>
<!-- 价格信息 --> <!-- Price Section -->
<div class="price-section" v-if="getEntryPrice(signal) || signal.stop_loss || signal.take_profit"> <div class="price-section" v-if="getEntryPrice(signal) || signal.stop_loss || signal.take_profit">
<div class="price-item" v-if="getEntryPrice(signal)"> <div class="price-item" v-if="getEntryPrice(signal)">
<div class="price-label">{{ getEntryPriceLabel(signal) }}</div> <div class="price-label">{{ getEntryPriceLabel(signal) }}</div>
@ -712,7 +598,7 @@
</div> </div>
</div> </div>
<!-- 信号详情 --> <!-- Details -->
<div class="signal-details" v-if="signal.signal_type_detail || signal.entry_type || signal.position_size || signal.current_price"> <div class="signal-details" v-if="signal.signal_type_detail || signal.entry_type || signal.position_size || signal.current_price">
<div class="detail-row" v-if="signal.current_price"> <div class="detail-row" v-if="signal.current_price">
<span class="detail-label">当前价:</span> <span class="detail-label">当前价:</span>
@ -736,13 +622,13 @@
</div> </div>
</div> </div>
<!-- 分析理由(合并 reason 和 analysis_summary --> <!-- Reason -->
<div class="signal-reason" v-if="getCombinedReason(signal)"> <div class="signal-reason" v-if="getCombinedReason(signal)">
<div class="reason-label">分析理由</div> <div class="reason-label">分析理由</div>
<div class="reason-text">{{ getCombinedReason(signal) }}</div> <div class="reason-text">{{ getCombinedReason(signal) }}</div>
</div> </div>
<!-- 时间戳 --> <!-- Time -->
<div class="signal-time"> <div class="signal-time">
{{ formatTime(signal.timestamp || signal.created_at) }} {{ formatTime(signal.timestamp || signal.created_at) }}
</div> </div>
@ -750,7 +636,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script> <script>
@ -866,17 +751,13 @@
return map[size] || size; return map[size] || size;
}, },
// 获取入场价格(限价单显示挂单价格,市价单显示入场价)
getEntryPrice(signal) { getEntryPrice(signal) {
// 如果是限价单且有挂单价格,优先显示挂单价格
if (signal.entry_type === 'limit' && signal.entry_zone) { if (signal.entry_type === 'limit' && signal.entry_zone) {
return signal.entry_zone; return signal.entry_zone;
} }
// 否则显示普通入场价
return signal.entry_price; return signal.entry_price;
}, },
// 获取入场价格标签
getEntryPriceLabel(signal) { getEntryPriceLabel(signal) {
if (signal.entry_type === 'limit' && signal.entry_zone) { if (signal.entry_type === 'limit' && signal.entry_zone) {
return '挂单价'; return '挂单价';
@ -884,7 +765,6 @@
return '入场价'; return '入场价';
}, },
// 获取新闻情绪文本
getNewsSentimentText(sentiment) { getNewsSentimentText(sentiment) {
const map = { const map = {
'bullish': '看涨', 'bullish': '看涨',
@ -896,27 +776,12 @@
return map[sentiment] || sentiment; return map[sentiment] || sentiment;
}, },
// 格式化价位数据
formatLevels(levels) {
if (Array.isArray(levels)) {
return levels.map(l => '$' + l.toFixed(2)).join(', ');
} else if (typeof levels === 'number') {
return '$' + levels.toFixed(2);
} else if (typeof levels === 'string') {
return levels;
}
return '';
},
// 合并理由和分析摘要
getCombinedReason(signal) { getCombinedReason(signal) {
// 优先使用 analysis_summary如果不存在则使用 reason
return signal.analysis_summary || signal.reason || ''; return signal.analysis_summary || signal.reason || '';
} }
}, },
mounted() { mounted() {
this.loadSignals(); this.loadSignals();
// 每30秒自动刷新
this.refreshInterval = setInterval(() => { this.refreshInterval = setInterval(() => {
this.loadSignals(); this.loadSignals();
}, 30000); }, 30000);

File diff suppressed because it is too large Load Diff