1
This commit is contained in:
parent
599a3498ba
commit
e5fa1f2024
@ -8,145 +8,102 @@
|
||||
<!-- 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=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>
|
||||
/* ========================================
|
||||
ADMIN PAGE - ADVANCED STYLING
|
||||
======================================== */
|
||||
:root {
|
||||
--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 {
|
||||
background: var(--bg-primary);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
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; }
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#app {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.admin-page {
|
||||
min-height: 100vh;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* === GLASSMORPHISM SIDEBAR === */
|
||||
/* Admin Layout */
|
||||
.admin-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.admin-sidebar {
|
||||
width: 260px;
|
||||
background: rgba(17, 24, 39, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-right: 1px solid rgba(0, 240, 255, 0.2);
|
||||
box-shadow: 4px 0 32px rgba(0, 0, 0, 0.5);
|
||||
width: 240px;
|
||||
background: var(--bg-primary);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 24px 0;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.admin-logo {
|
||||
padding: 24px 24px 28px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 20px;
|
||||
padding: 0 24px 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.admin-logo h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
font-family: 'DM Serif Display', serif;
|
||||
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;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.admin-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-nav-item {
|
||||
padding: 16px 24px;
|
||||
padding: 12px 24px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
transition: all 0.2s;
|
||||
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 {
|
||||
background: rgba(26, 31, 58, 0.6);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border-left-color: rgba(0, 240, 255, 0.5);
|
||||
}
|
||||
|
||||
.admin-nav-item:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.admin-nav-item.active {
|
||||
background: rgba(26, 31, 58, 0.9);
|
||||
color: var(--accent);
|
||||
border-left-color: var(--accent);
|
||||
box-shadow: inset 0 0 20px rgba(0, 240, 255, 0.1);
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
border-left-color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.admin-nav-item.active::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* === MAIN CONTENT === */
|
||||
/* Main Content */
|
||||
.admin-main {
|
||||
margin-left: 260px;
|
||||
margin-left: 240px;
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
.admin-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
@ -154,61 +111,33 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.admin-title {
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
font-family: 'DM Serif Display', serif;
|
||||
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
padding: 10px 20px;
|
||||
background: rgba(26, 31, 58, 0.6);
|
||||
border: 1px solid rgba(255, 0, 64, 0.3);
|
||||
border-radius: 12px;
|
||||
color: #ff0040;
|
||||
font-size: 13px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
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;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: rgba(255, 0, 64, 0.15);
|
||||
border-color: rgba(255, 0, 64, 0.6);
|
||||
box-shadow: 0 0 16px rgba(255, 0, 64, 0.3);
|
||||
transform: translateY(-2px);
|
||||
background: var(--error-light);
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.logout-btn:active::before {
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
}
|
||||
|
||||
/* === STATS GRID === */
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
@ -217,38 +146,10 @@
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: rgba(26, 31, 58, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
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 {
|
||||
@ -260,179 +161,137 @@
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36px;
|
||||
font-weight: 300;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
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;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 12px 18px;
|
||||
background: rgba(26, 31, 58, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
color: var(--text-primary);
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
background: rgba(26, 31, 58, 0.9);
|
||||
border-color: rgba(0, 240, 255, 0.4);
|
||||
box-shadow: 0 0 24px rgba(0, 240, 255, 0.2);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 12px 28px;
|
||||
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
|
||||
padding: 12px 24px;
|
||||
background: var(--primary);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 16px rgba(0, 240, 255, 0.3);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
box-shadow: 0 6px 24px rgba(0, 240, 255, 0.5);
|
||||
transform: translateY(-2px);
|
||||
background: #0052CC;
|
||||
}
|
||||
|
||||
.search-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* === USERS TABLE === */
|
||||
/* Table */
|
||||
.users-table {
|
||||
width: 100%;
|
||||
background: rgba(26, 31, 58, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.users-table table {
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.users-table th {
|
||||
background: rgba(10, 14, 39, 0.6);
|
||||
padding: 16px 20px;
|
||||
th {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.users-table td {
|
||||
padding: 16px 20px;
|
||||
td {
|
||||
padding: 14px 16px;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.users-table tr:last-child td {
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.users-table tr {
|
||||
transition: all 0.2s ease;
|
||||
tr:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.users-table tr:hover {
|
||||
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;
|
||||
padding: 6px 16px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: linear-gradient(135deg, rgba(0, 200, 81, 0.2) 0%, rgba(0, 230, 118, 0.2) 100%);
|
||||
color: #00C851;
|
||||
border: 1px solid rgba(0, 200, 81, 0.4);
|
||||
box-shadow: 0 0 12px rgba(0, 200, 81, 0.2);
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background: linear-gradient(135deg, rgba(255, 0, 64, 0.2) 0%, rgba(255, 107, 107, 0.2) 100%);
|
||||
color: #FF0040;
|
||||
border: 1px solid rgba(255, 0, 64, 0.4);
|
||||
box-shadow: 0 0 12px rgba(255, 0, 64, 0.2);
|
||||
background: var(--error-light);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* === PAGINATION === */
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 10px 20px;
|
||||
background: rgba(26, 31, 58, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 8px 16px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.pagination button:hover:not(:disabled) {
|
||||
background: rgba(0, 240, 255, 0.15);
|
||||
border-color: rgba(0, 240, 255, 0.4);
|
||||
box-shadow: 0 0 16px rgba(0, 240, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
background: var(--primary-light);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
@ -440,131 +299,92 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination .page-info {
|
||||
.page-info {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* === LOGIN OVERLAY === */
|
||||
/* Login Overlay */
|
||||
.login-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(10, 14, 39, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
max-width: 400px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 12px;
|
||||
padding: 48px;
|
||||
background: rgba(26, 31, 58, 0.95);
|
||||
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);
|
||||
}
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.login-box h2 {
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
font-family: 'DM Serif Display', serif;
|
||||
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-box input {
|
||||
width: 100%;
|
||||
padding: 14px 18px;
|
||||
background: rgba(10, 14, 39, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
color: var(--text-primary);
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s ease;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.login-box input:focus {
|
||||
outline: none;
|
||||
background: rgba(10, 14, 39, 0.9);
|
||||
border-color: rgba(0, 240, 255, 0.4);
|
||||
box-shadow: 0 0 24px rgba(0, 240, 255, 0.2);
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.login-box input::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.login-box button {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
|
||||
padding: 14px;
|
||||
background: var(--primary);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 16px rgba(0, 240, 255, 0.3);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.login-box button:hover {
|
||||
box-shadow: 0 6px 24px rgba(0, 240, 255, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.login-box button:active {
|
||||
transform: translateY(0);
|
||||
background: #0052CC;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 14px;
|
||||
background: rgba(255, 0, 64, 0.15);
|
||||
border: 1px solid rgba(255, 0, 64, 0.4);
|
||||
border-radius: 12px;
|
||||
color: #ff0040;
|
||||
padding: 12px 16px;
|
||||
background: var(--error-light);
|
||||
border: 1px solid var(--error);
|
||||
border-radius: 8px;
|
||||
color: var(--error);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 0 16px rgba(255, 0, 64, 0.2);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* === RESPONSIVE === */
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.admin-sidebar {
|
||||
width: 200px;
|
||||
@ -572,27 +392,18 @@
|
||||
|
||||
.admin-main {
|
||||
margin-left: 200px;
|
||||
min-width: 600px;
|
||||
padding: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.admin-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 登录遮罩 -->
|
||||
<!-- Login Overlay -->
|
||||
<div v-if="!authenticated" class="login-overlay">
|
||||
<div class="login-box">
|
||||
<h2>后台管理</h2>
|
||||
@ -609,99 +420,100 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理页面 -->
|
||||
<!-- Admin Page -->
|
||||
<div v-if="authenticated" class="admin-page">
|
||||
<!-- 侧边栏 -->
|
||||
<!-- Sidebar -->
|
||||
<div class="admin-sidebar">
|
||||
<div class="admin-logo">
|
||||
<h2>Tradus 管理后台</h2>
|
||||
</div>
|
||||
<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 class="admin-nav-item" :class="{active: currentView === 'users'}" @click="currentView = 'users'">
|
||||
<li class="admin-nav-item"
|
||||
:class="{active: currentView === 'users'}"
|
||||
@click="currentView = 'users'">
|
||||
用户管理
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<!-- Main Content -->
|
||||
<div class="admin-main">
|
||||
<div class="admin-container">
|
||||
<div class="admin-header">
|
||||
<h1 class="admin-title">{{ currentViewTitle }}</h1>
|
||||
<button class="logout-btn" @click="logout">退出</button>
|
||||
</div>
|
||||
<div class="admin-header">
|
||||
<h1 class="admin-title">{{ currentViewTitle }}</h1>
|
||||
<button class="logout-btn" @click="logout">退出</button>
|
||||
</div>
|
||||
|
||||
<!-- 数据统计视图 -->
|
||||
<div v-if="currentView === 'dashboard'">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">用户总数</div>
|
||||
<div class="stat-value">{{ stats.total_users }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">活跃用户(7天)</div>
|
||||
<div class="stat-value">{{ stats.active_users }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">对话总数</div>
|
||||
<div class="stat-value">{{ stats.total_conversations }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">消息总数</div>
|
||||
<div class="stat-value">{{ stats.total_messages }}</div>
|
||||
</div>
|
||||
<!-- Dashboard View -->
|
||||
<div v-if="currentView === 'dashboard'">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">用户总数</div>
|
||||
<div class="stat-value">{{ stats.total_users }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">活跃用户(7天)</div>
|
||||
<div class="stat-value">{{ stats.active_users }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">对话总数</div>
|
||||
<div class="stat-value">{{ stats.total_conversations }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">消息总数</div>
|
||||
<div class="stat-value">{{ stats.total_messages }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户管理视图 -->
|
||||
<div v-if="currentView === 'users'">
|
||||
<h3 class="section-title">用户列表</h3>
|
||||
<div class="search-bar">
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索手机号..."
|
||||
@keyup.enter="searchUsers"
|
||||
>
|
||||
<button class="search-btn" @click="searchUsers">搜索</button>
|
||||
</div>
|
||||
<!-- Users View -->
|
||||
<div v-if="currentView === 'users'">
|
||||
<div class="search-bar">
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索手机号..."
|
||||
@keyup.enter="searchUsers"
|
||||
>
|
||||
<button class="search-btn" @click="searchUsers">搜索</button>
|
||||
</div>
|
||||
|
||||
<div class="users-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>手机号</th>
|
||||
<th>注册时间</th>
|
||||
<th>最后登录</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.phone }}</td>
|
||||
<td>{{ formatDate(user.created_at) }}</td>
|
||||
<td>{{ formatDate(user.last_login_at) }}</td>
|
||||
<td>
|
||||
<span :class="['status-badge', user.is_active ? 'status-active' : 'status-inactive']">
|
||||
{{ user.is_active ? '正常' : '禁用' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="users-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>手机号</th>
|
||||
<th>注册时间</th>
|
||||
<th>最后登录</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.phone }}</td>
|
||||
<td>{{ formatDate(user.created_at) }}</td>
|
||||
<td>{{ formatDate(user.last_login_at) }}</td>
|
||||
<td>
|
||||
<span :class="['status-badge', user.is_active ? 'status-active' : 'status-inactive']">
|
||||
{{ user.is_active ? '正常' : '禁用' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<button @click="prevPage" :disabled="currentPage <= 1">上一页</button>
|
||||
<span class="page-info">第 {{ currentPage }} / {{ totalPages }} 页</span>
|
||||
<button @click="nextPage" :disabled="currentPage >= totalPages">下一页</button>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<button @click="prevPage" :disabled="currentPage <= 1">上一页</button>
|
||||
<span class="page-info">第 {{ currentPage }} / {{ totalPages }} 页</span>
|
||||
<button @click="nextPage" :disabled="currentPage >= totalPages">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -719,7 +531,7 @@
|
||||
password: '',
|
||||
errorMessage: '',
|
||||
adminPassword: '',
|
||||
currentView: 'users', // 当前视图:dashboard 或 users
|
||||
currentView: 'users',
|
||||
stats: {
|
||||
total_users: 0,
|
||||
active_users: 0,
|
||||
@ -744,7 +556,6 @@
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 检查是否已登录
|
||||
const savedPassword = sessionStorage.getItem('admin_password');
|
||||
if (savedPassword) {
|
||||
this.adminPassword = savedPassword;
|
||||
@ -851,11 +662,8 @@
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
|
||||
// 直接解析 ISO 格式的时间字符串,不做时区转换
|
||||
// 假设服务器返回的是 UTC 时间,我们将其视为本地时间显示
|
||||
const date = new Date(dateString + 'Z'); // 添加 Z 表示 UTC
|
||||
const date = new Date(dateString + 'Z');
|
||||
|
||||
// 获取各个时间部分
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,182 +3,115 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - Tradus|AI 金融智能体</title>
|
||||
<title>登录 - Tradus AI</title>
|
||||
|
||||
<!-- 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=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>
|
||||
/* ========================================
|
||||
LOGIN PAGE - ADVANCED STYLING
|
||||
======================================== */
|
||||
:root {
|
||||
--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 {
|
||||
background: var(--bg-primary);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
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 {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* === GLASSMORPHISM LOGIN CONTAINER === */
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 56px 48px;
|
||||
background: rgba(26, 31, 58, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
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;
|
||||
max-width: 420px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 12px;
|
||||
padding: 48px 40px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.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 {
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-logo svg {
|
||||
color: var(--accent);
|
||||
filter: drop-shadow(0 0 12px rgba(0, 240, 255, 0.6));
|
||||
animation: logoFloat 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes logoFloat {
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||
50% { transform: translateY(-8px) rotate(5deg); }
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
font-family: 'DM Serif Display', serif;
|
||||
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;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* === FORM === */
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 14px 18px;
|
||||
background: rgba(10, 14, 39, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
color: var(--text-primary);
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
font-family: inherit;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
background: rgba(10, 14, 39, 0.9);
|
||||
border-color: rgba(0, 240, 255, 0.5);
|
||||
box-shadow: 0 0 24px rgba(0, 240, 255, 0.25);
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px var(--primary-light);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.code-input-group {
|
||||
@ -191,43 +124,20 @@
|
||||
}
|
||||
|
||||
.send-code-btn {
|
||||
padding: 14px 24px;
|
||||
background: rgba(26, 31, 58, 0.6);
|
||||
border: 1px solid rgba(0, 240, 255, 0.3);
|
||||
border-radius: 12px;
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 12px 20px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 8px;
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.3s ease;
|
||||
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;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.send-code-btn:hover:not(:disabled) {
|
||||
background: rgba(0, 240, 255, 0.15);
|
||||
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%;
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.send-code-btn:disabled {
|
||||
@ -237,83 +147,50 @@
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
|
||||
padding: 14px;
|
||||
background: var(--primary);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: var(--bg-primary);
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
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;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.login-btn:hover:not(:disabled) {
|
||||
box-shadow: 0 6px 32px rgba(0, 240, 255, 0.6);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.login-btn:hover:not(:disabled)::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.login-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
background: #0052CC;
|
||||
}
|
||||
|
||||
.login-btn:disabled {
|
||||
opacity: 0.5;
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 14px 18px;
|
||||
background: rgba(255, 0, 64, 0.15);
|
||||
border: 1px solid rgba(255, 0, 64, 0.4);
|
||||
border-radius: 12px;
|
||||
color: #ff0040;
|
||||
padding: 12px 16px;
|
||||
background: var(--error-light);
|
||||
border: 1px solid var(--error);
|
||||
border-radius: 8px;
|
||||
color: var(--error);
|
||||
font-size: 13px;
|
||||
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 {
|
||||
margin-top: 32px;
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* === RESPONSIVE === */
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
padding: 40px 32px;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 26px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.code-input-group {
|
||||
@ -324,26 +201,6 @@
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
@ -351,7 +208,7 @@
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<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"/>
|
||||
</svg>
|
||||
</div>
|
||||
@ -500,9 +357,7 @@
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.token) {
|
||||
// 保存token
|
||||
localStorage.setItem('token', data.token);
|
||||
// 跳转到主页
|
||||
window.location.href = '/app';
|
||||
} else {
|
||||
this.errorMessage = data.message || '登录失败';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,130 +2,104 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>交易信号 - Stock Agent</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>交易信号 - Tradus</title>
|
||||
|
||||
<!-- 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=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>
|
||||
/* ===== 页面基础 ===== */
|
||||
html, body {
|
||||
overflow-x: hidden;
|
||||
max-width: 100vw;
|
||||
background: linear-gradient(180deg, #0A0E27 0%, #000000 100%);
|
||||
:root {
|
||||
--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: #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 {
|
||||
height: auto;
|
||||
display: block;
|
||||
align-items: initial;
|
||||
justify-content: initial;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.signals-page {
|
||||
min-height: 100vh;
|
||||
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;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 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);
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.signals-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.signals-title {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
font-family: 'DM Serif Display', serif;
|
||||
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.signals-title::before {
|
||||
content: '';
|
||||
.live-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #00F0FF;
|
||||
background: var(--success);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 12px #00F0FF, 0 0 24px #00F0FF;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.2); }
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 10px 20px;
|
||||
background: rgba(0, 240, 255, 0.1);
|
||||
border: 1px solid rgba(0, 240, 255, 0.3);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: #00F0FF;
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: rgba(0, 240, 255, 0.2);
|
||||
border-color: #00F0FF;
|
||||
box-shadow: 0 0 20px rgba(0, 240, 255, 0.4);
|
||||
transform: translateY(-2px);
|
||||
background: var(--primary-light);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* ===== 统计卡片网格 ===== */
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
@ -134,109 +108,102 @@
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
position: relative;
|
||||
background: rgba(26, 31, 58, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
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 {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
background: linear-gradient(135deg, #00F0FF 0%, #00C9FF 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.stat-sub {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stat-value.positive {
|
||||
background: linear-gradient(135deg, #00C851 0%, #00E676 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.stat-value.negative {
|
||||
background: linear-gradient(135deg, #FF4444 0%, #FF6B6B 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* ===== 标签页导航 ===== */
|
||||
/* 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 {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
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 {
|
||||
flex: 1;
|
||||
padding: 14px 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text-primary);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: linear-gradient(135deg, rgba(0, 240, 255, 0.15) 0%, rgba(102, 126, 234, 0.15) 100%);
|
||||
color: #00F0FF;
|
||||
box-shadow: 0 0 20px rgba(0, 240, 255, 0.2);
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
/* 信号卡片网格 */
|
||||
/* Signals Grid */
|
||||
.signals-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
@ -244,16 +211,16 @@
|
||||
}
|
||||
|
||||
.signal-card {
|
||||
background: var(--bg-secondary);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.signal-card:hover {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 4px 12px rgba(0, 200, 150, 0.1);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.signal-header {
|
||||
@ -270,8 +237,8 @@
|
||||
}
|
||||
|
||||
.signal-symbol {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@ -280,7 +247,7 @@
|
||||
padding: 3px 8px;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.signal-action-group {
|
||||
@ -291,55 +258,55 @@
|
||||
|
||||
.signal-action {
|
||||
padding: 6px 16px;
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.signal-action.buy {
|
||||
background: rgba(0, 255, 65, 0.1);
|
||||
color: #00ff41;
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.signal-action.sell {
|
||||
background: rgba(255, 68, 68, 0.1);
|
||||
color: #ff4444;
|
||||
background: var(--error-light);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.signal-action.hold {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.signal-grade {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.signal-grade.A {
|
||||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 179, 0, 0.2));
|
||||
color: gold;
|
||||
background: var(--warning-light);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.signal-grade.B {
|
||||
background: linear-gradient(135deg, rgba(192, 192, 192, 0.2), rgba(160, 160, 160, 0.2));
|
||||
color: silver;
|
||||
background: #E5E7EB;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.signal-grade.C {
|
||||
background: linear-gradient(135deg, rgba(205, 127, 50, 0.2), rgba(160, 82, 45, 0.2));
|
||||
color: #cd7f32;
|
||||
background: #FED7AA;
|
||||
color: #EA580C;
|
||||
}
|
||||
|
||||
.signal-grade.D {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-secondary);
|
||||
background: var(--error-light);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* 置信度条 */
|
||||
/* Confidence */
|
||||
.confidence-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@ -354,8 +321,8 @@
|
||||
}
|
||||
|
||||
.confidence-value {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.confidence-bar {
|
||||
@ -367,11 +334,11 @@
|
||||
|
||||
.confidence-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--accent) 0%, #00ff41 100%);
|
||||
background: var(--primary);
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
/* 价格信息 */
|
||||
/* Price Section */
|
||||
.price-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
@ -395,10 +362,10 @@
|
||||
.price-value {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
font-family: monospace;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* 信号详情 */
|
||||
/* Signal Details */
|
||||
.signal-details {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@ -419,7 +386,7 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 分析理由 */
|
||||
/* Reason */
|
||||
.signal-reason {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
@ -439,13 +406,13 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 时间戳 */
|
||||
/* Time */
|
||||
.signal-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
@ -456,32 +423,20 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
/* Loading */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.signals-page {
|
||||
padding: 10px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.signals-container {
|
||||
min-width: auto;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 取消顶部固定 */
|
||||
.sticky-header {
|
||||
position: static;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.signals-header {
|
||||
@ -491,12 +446,7 @@
|
||||
}
|
||||
|
||||
.signals-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.signals-title span {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
@ -505,18 +455,15 @@
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 12px;
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
word-break: break-all;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@ -534,218 +481,156 @@
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="signals-page">
|
||||
<div class="signals-container">
|
||||
<!-- 固定顶部区域 -->
|
||||
<div class="sticky-header">
|
||||
<!-- 头部 -->
|
||||
<div class="signals-header">
|
||||
<h1 class="signals-title">交易信号中心 <span>| Trading Signals</span></h1>
|
||||
<button class="refresh-btn" @click="loadSignals">刷新</button>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<div class="signals-header">
|
||||
<h1 class="signals-title">
|
||||
交易信号中心
|
||||
<span class="live-dot"></span>
|
||||
</h1>
|
||||
<button class="refresh-btn" @click="loadSignals">刷新</button>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">加密货币信号</div>
|
||||
<div class="stat-value">{{ stats.crypto.total }}</div>
|
||||
<div class="stat-label" style="margin-top: 6px;">最近24小时: {{ stats.crypto.recent_24h }}</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')">
|
||||
加密货币 ({{ cryptoSignals.length }})
|
||||
</button>
|
||||
<button class="tab" :class="{ active: currentTab === 'stock' }" @click="switchTab('stock')">
|
||||
美股 ({{ stockSignals.length }})
|
||||
</button>
|
||||
<button class="tab" :class="{ active: currentTab === 'all' }" @click="switchTab('all')">
|
||||
全部信号 ({{ allSignals.length }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Signals List -->
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-else-if="currentSignals.length === 0" class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 6v6l4 2"/>
|
||||
</svg>
|
||||
<p>暂无信号</p>
|
||||
</div>
|
||||
<div v-else class="signals-grid">
|
||||
<div v-for="signal in currentSignals" :key="signal.id" class="signal-card">
|
||||
<!-- Header -->
|
||||
<div class="signal-header">
|
||||
<div class="signal-symbol-group">
|
||||
<span class="signal-symbol">{{ signal.symbol }}</span>
|
||||
<span class="signal-type-badge">{{ signal.signal_type === 'crypto' ? '加密货币' : '美股' }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">美股信号</div>
|
||||
<div class="stat-value">{{ stats.stock.total }}</div>
|
||||
<div class="stat-label" style="margin-top: 6px;">最近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 class="signal-action-group">
|
||||
<span class="signal-action" :class="signal.action">
|
||||
{{ signal.action === 'buy' ? '做多' : signal.action === 'sell' ? '做空' : '持有' }}
|
||||
</span>
|
||||
<span class="signal-grade" :class="signal.grade">{{ signal.grade }}级</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 等级统计 -->
|
||||
<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>
|
||||
<!-- Confidence -->
|
||||
<div class="confidence-section">
|
||||
<div class="confidence-label">
|
||||
<span>置信度</span>
|
||||
<span class="confidence-value">{{ signal.confidence }}%</span>
|
||||
</div>
|
||||
<div class="confidence-bar">
|
||||
<div class="confidence-fill" :style="{ width: signal.confidence + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<div class="tabs">
|
||||
<button class="tab" :class="{ active: currentTab === 'crypto' }" @click="switchTab('crypto')">
|
||||
加密货币 ({{ cryptoSignals.length }})
|
||||
</button>
|
||||
<button class="tab" :class="{ active: currentTab === 'stock' }" @click="switchTab('stock')">
|
||||
美股 ({{ stockSignals.length }})
|
||||
</button>
|
||||
<button class="tab" :class="{ active: currentTab === 'all' }" @click="switchTab('all')">
|
||||
全部信号 ({{ allSignals.length }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 信号列表 -->
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-else-if="currentSignals.length === 0" class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 6v6l4 2"/>
|
||||
</svg>
|
||||
<p>暂无信号</p>
|
||||
</div>
|
||||
<div v-else class="signals-grid">
|
||||
<div v-for="signal in currentSignals" :key="signal.id" class="signal-card" :class="signal.action">
|
||||
<!-- 信号头部 -->
|
||||
<div class="signal-header">
|
||||
<div class="signal-symbol-group">
|
||||
<span class="signal-symbol">{{ signal.symbol }}</span>
|
||||
<span class="signal-type-badge">{{ signal.signal_type === 'crypto' ? '加密货币' : '美股' }}</span>
|
||||
</div>
|
||||
<div class="signal-action-group">
|
||||
<span class="signal-action" :class="signal.action">
|
||||
{{ signal.action === 'buy' ? '做多' : signal.action === 'sell' ? '做空' : '持有' }}
|
||||
</span>
|
||||
<span class="signal-grade" :class="signal.grade">{{ signal.grade }}级</span>
|
||||
</div>
|
||||
<!-- Price Section -->
|
||||
<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-label">{{ getEntryPriceLabel(signal) }}</div>
|
||||
<div class="price-value">${{ getEntryPrice(signal).toFixed(2) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 置信度 -->
|
||||
<div class="confidence-section">
|
||||
<div class="confidence-label">
|
||||
<span>置信度</span>
|
||||
<span class="confidence-value">{{ signal.confidence }}%</span>
|
||||
</div>
|
||||
<div class="confidence-bar">
|
||||
<div class="confidence-fill" :style="{ width: signal.confidence + '%' }"></div>
|
||||
</div>
|
||||
<div class="price-item" v-if="signal.stop_loss">
|
||||
<div class="price-label">止损</div>
|
||||
<div class="price-value">${{ signal.stop_loss?.toFixed(2) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 价格信息 -->
|
||||
<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-label">{{ getEntryPriceLabel(signal) }}</div>
|
||||
<div class="price-value">${{ getEntryPrice(signal).toFixed(2) }}</div>
|
||||
</div>
|
||||
<div class="price-item" v-if="signal.stop_loss">
|
||||
<div class="price-label">止损</div>
|
||||
<div class="price-value">${{ signal.stop_loss?.toFixed(2) }}</div>
|
||||
</div>
|
||||
<div class="price-item" v-if="signal.take_profit">
|
||||
<div class="price-label">止盈</div>
|
||||
<div class="price-value">${{ signal.take_profit?.toFixed(2) }}</div>
|
||||
</div>
|
||||
<div class="price-item" v-if="signal.take_profit">
|
||||
<div class="price-label">止盈</div>
|
||||
<div class="price-value">${{ signal.take_profit?.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 信号详情 -->
|
||||
<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">
|
||||
<span class="detail-label">当前价:</span>
|
||||
<span class="detail-value">${{ signal.current_price?.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="signal.signal_type_detail">
|
||||
<span class="detail-label">周期:</span>
|
||||
<span class="detail-value">{{ getSignalTypeText(signal.signal_type_detail) }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="signal.entry_type">
|
||||
<span class="detail-label">入场:</span>
|
||||
<span class="detail-value">{{ getEntryTypeText(signal.entry_type) }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="signal.position_size">
|
||||
<span class="detail-label">仓位:</span>
|
||||
<span class="detail-value">{{ getPositionSizeText(signal.position_size) }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="signal.news_sentiment">
|
||||
<span class="detail-label">情绪:</span>
|
||||
<span class="detail-value">{{ getNewsSentimentText(signal.news_sentiment) }}</span>
|
||||
</div>
|
||||
<!-- Details -->
|
||||
<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">
|
||||
<span class="detail-label">当前价:</span>
|
||||
<span class="detail-value">${{ signal.current_price?.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="signal.signal_type_detail">
|
||||
<span class="detail-label">周期:</span>
|
||||
<span class="detail-value">{{ getSignalTypeText(signal.signal_type_detail) }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="signal.entry_type">
|
||||
<span class="detail-label">入场:</span>
|
||||
<span class="detail-value">{{ getEntryTypeText(signal.entry_type) }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="signal.position_size">
|
||||
<span class="detail-label">仓位:</span>
|
||||
<span class="detail-value">{{ getPositionSizeText(signal.position_size) }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="signal.news_sentiment">
|
||||
<span class="detail-label">情绪:</span>
|
||||
<span class="detail-value">{{ getNewsSentimentText(signal.news_sentiment) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分析理由(合并 reason 和 analysis_summary) -->
|
||||
<div class="signal-reason" v-if="getCombinedReason(signal)">
|
||||
<div class="reason-label">分析理由</div>
|
||||
<div class="reason-text">{{ getCombinedReason(signal) }}</div>
|
||||
</div>
|
||||
<!-- Reason -->
|
||||
<div class="signal-reason" v-if="getCombinedReason(signal)">
|
||||
<div class="reason-label">分析理由</div>
|
||||
<div class="reason-text">{{ getCombinedReason(signal) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间戳 -->
|
||||
<div class="signal-time">
|
||||
{{ formatTime(signal.timestamp || signal.created_at) }}
|
||||
</div>
|
||||
<!-- Time -->
|
||||
<div class="signal-time">
|
||||
{{ formatTime(signal.timestamp || signal.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -866,17 +751,13 @@
|
||||
return map[size] || size;
|
||||
},
|
||||
|
||||
// 获取入场价格(限价单显示挂单价格,市价单显示入场价)
|
||||
getEntryPrice(signal) {
|
||||
// 如果是限价单且有挂单价格,优先显示挂单价格
|
||||
if (signal.entry_type === 'limit' && signal.entry_zone) {
|
||||
return signal.entry_zone;
|
||||
}
|
||||
// 否则显示普通入场价
|
||||
return signal.entry_price;
|
||||
},
|
||||
|
||||
// 获取入场价格标签
|
||||
getEntryPriceLabel(signal) {
|
||||
if (signal.entry_type === 'limit' && signal.entry_zone) {
|
||||
return '挂单价';
|
||||
@ -884,7 +765,6 @@
|
||||
return '入场价';
|
||||
},
|
||||
|
||||
// 获取新闻情绪文本
|
||||
getNewsSentimentText(sentiment) {
|
||||
const map = {
|
||||
'bullish': '看涨',
|
||||
@ -896,27 +776,12 @@
|
||||
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) {
|
||||
// 优先使用 analysis_summary,如果不存在则使用 reason
|
||||
return signal.analysis_summary || signal.reason || '';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadSignals();
|
||||
// 每30秒自动刷新
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.loadSignals();
|
||||
}, 30000);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user