1144 lines
43 KiB
HTML
1144 lines
43 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>实盘交易 | Tradus Auto Trading</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 rel="stylesheet" href="/static/css/style.css">
|
||
<style>
|
||
/* ========================================
|
||
REAL-TRADING PAGE - ADVANCED STYLING
|
||
======================================== */
|
||
|
||
html, body {
|
||
overflow-x: hidden;
|
||
max-width: 100vw;
|
||
}
|
||
|
||
/* === ATMOSPHERIC BACKGROUND === */
|
||
body {
|
||
background: var(--bg-primary);
|
||
position: relative;
|
||
}
|
||
|
||
body::before {
|
||
content: '';
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-image:
|
||
radial-gradient(circle at 10% 15%, rgba(255, 68, 68, 0.12) 0%, transparent 50%),
|
||
radial-gradient(circle at 90% 85%, rgba(255, 68, 68, 0.08) 0%, transparent 50%),
|
||
radial-gradient(circle at 50% 50%, rgba(102, 126, 234, 0.06) 0%, transparent 60%);
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
animation: pulseRed 6s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulseRed {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.6; }
|
||
}
|
||
|
||
#app {
|
||
position: relative;
|
||
z-index: 1;
|
||
height: auto;
|
||
display: block;
|
||
align-items: initial;
|
||
justify-content: initial;
|
||
padding: 0;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.trading-page {
|
||
min-height: 100vh;
|
||
background: transparent;
|
||
padding: 20px;
|
||
}
|
||
|
||
.trading-container {
|
||
max-width: 1400px;
|
||
min-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* === GLASSMORPHISM HEADER === */
|
||
.sticky-header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
background: rgba(10, 14, 39, 0.9);
|
||
backdrop-filter: blur(20px);
|
||
-webkit-backdrop-filter: blur(20px);
|
||
border-bottom: 1px solid rgba(255, 68, 68, 0.3);
|
||
box-shadow: 0 4px 32px rgba(255, 68, 68, 0.2);
|
||
padding-bottom: 10px;
|
||
animation: slideDown 0.5s ease-out;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.trading-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding: 10px 0 20px 0;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
background: transparent;
|
||
}
|
||
|
||
.trading-title {
|
||
font-size: 28px;
|
||
font-weight: 300;
|
||
color: var(--text-primary);
|
||
font-family: 'DM Serif Display', serif;
|
||
letter-spacing: 0.5px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.trading-title .real-badge {
|
||
display: inline-block;
|
||
background: linear-gradient(135deg, #FF4444 0%, #FF6B6B 100%);
|
||
color: white;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
font-family: 'DM Sans', sans-serif;
|
||
animation: pulseBadge 2s ease-in-out infinite;
|
||
box-shadow: 0 0 16px rgba(255, 68, 68, 0.6);
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
@keyframes pulseBadge {
|
||
0%, 100% {
|
||
opacity: 1;
|
||
box-shadow: 0 0 16px rgba(255, 68, 68, 0.6);
|
||
}
|
||
50% {
|
||
opacity: 0.8;
|
||
box-shadow: 0 0 24px rgba(255, 68, 68, 0.8);
|
||
}
|
||
}
|
||
|
||
.refresh-btn {
|
||
padding: 10px 20px;
|
||
background: rgba(26, 31, 58, 0.6);
|
||
border: 1px solid rgba(0, 240, 255, 0.3);
|
||
color: var(--accent);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
border-radius: 8px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.refresh-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;
|
||
}
|
||
|
||
.refresh-btn:hover {
|
||
background: rgba(0, 240, 255, 0.15);
|
||
border-color: rgba(0, 240, 255, 0.6);
|
||
box-shadow: 0 0 24px rgba(0, 240, 255, 0.4);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.refresh-btn:active::before {
|
||
width: 300%;
|
||
height: 300%;
|
||
}
|
||
|
||
/* === ACCOUNT INFO CARDS === */
|
||
.account-info {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.account-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;
|
||
padding: 20px;
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.account-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 3px;
|
||
background: linear-gradient(90deg, #FF4444 0%, #FF6B6B 100%);
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.account-card:hover {
|
||
background: rgba(26, 31, 58, 0.9);
|
||
border-color: rgba(255, 68, 68, 0.3);
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 20px rgba(255, 68, 68, 0.2);
|
||
transform: translateY(-4px);
|
||
}
|
||
|
||
.account-card:hover::before {
|
||
opacity: 1;
|
||
}
|
||
|
||
.account-label {
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 8px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.account-value {
|
||
font-size: 28px;
|
||
font-weight: 300;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
color: var(--text-primary);
|
||
margin-bottom: 4px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.account-value.positive {
|
||
background: linear-gradient(135deg, #00C851 0%, #00E676 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
.account-value.negative {
|
||
background: linear-gradient(135deg, #FF4444 0%, #FF6B6B 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
.account-sub {
|
||
font-size: 12px;
|
||
color: var(--text-tertiary);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* === WARNING BANNERS === */
|
||
.warning-banner {
|
||
background: rgba(255, 68, 68, 0.15);
|
||
border: 1px solid rgba(255, 68, 68, 0.4);
|
||
border-radius: 12px;
|
||
padding: 16px 20px;
|
||
margin-bottom: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
animation: fadeIn 0.5s ease-out;
|
||
box-shadow: 0 4px 16px rgba(255, 68, 68, 0.2);
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.warning-banner svg {
|
||
width: 24px;
|
||
height: 24px;
|
||
color: #ff4444;
|
||
flex-shrink: 0;
|
||
filter: drop-shadow(0 0 8px rgba(255, 68, 68, 0.6));
|
||
}
|
||
|
||
.warning-banner-text {
|
||
color: #ff4444;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* === TABS === */
|
||
.tabs {
|
||
display: flex;
|
||
gap: 0;
|
||
margin-bottom: 24px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.tab {
|
||
padding: 14px 28px;
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text-secondary);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
border-bottom: 2px solid transparent;
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
}
|
||
|
||
.tab:hover {
|
||
color: var(--text-primary);
|
||
background: rgba(255, 255, 255, 0.02);
|
||
}
|
||
|
||
.tab.active {
|
||
color: var(--accent);
|
||
border-bottom-color: var(--accent);
|
||
background: rgba(0, 240, 255, 0.05);
|
||
}
|
||
|
||
.tab.active::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: -1px;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 2px;
|
||
background: linear-gradient(90deg, transparent 0%, #00F0FF 50%, transparent 100%);
|
||
box-shadow: 0 0 12px rgba(0, 240, 255, 0.6);
|
||
}
|
||
|
||
/* === TABLE CONTAINER === */
|
||
.orders-table-container {
|
||
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;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.orders-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.orders-table th,
|
||
.orders-table td {
|
||
padding: 14px 16px;
|
||
text-align: left;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.orders-table th {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
color: var(--text-secondary);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.orders-table td {
|
||
color: var(--text-primary);
|
||
font-size: 13px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.orders-table tr {
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.orders-table tr:hover {
|
||
background: rgba(0, 240, 255, 0.05);
|
||
}
|
||
|
||
.orders-table tr:hover td {
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.orders-table tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
/* === BADGES === */
|
||
.side-badge {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.5px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.side-long {
|
||
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);
|
||
}
|
||
|
||
.side-short {
|
||
background: linear-gradient(135deg, rgba(255, 68, 68, 0.2) 0%, rgba(255, 107, 107, 0.2) 100%);
|
||
color: #FF4444;
|
||
border: 1px solid rgba(255, 68, 68, 0.4);
|
||
}
|
||
|
||
.grade-badge {
|
||
display: inline-block;
|
||
padding: 3px 10px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.grade-A {
|
||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||
color: #000;
|
||
box-shadow: 0 0 12px rgba(255, 215, 0, 0.4);
|
||
}
|
||
|
||
.grade-B {
|
||
background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%);
|
||
color: #000;
|
||
}
|
||
|
||
.grade-C {
|
||
background: linear-gradient(135deg, #CD7F32 0%, #B8860B 100%);
|
||
color: #000;
|
||
}
|
||
|
||
.grade-D {
|
||
background: rgba(255, 68, 68, 0.2);
|
||
color: #ff4444;
|
||
border: 1px solid rgba(255, 68, 68, 0.4);
|
||
}
|
||
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.status-open {
|
||
background: rgba(0, 200, 81, 0.15);
|
||
color: #00C851;
|
||
border: 1px solid rgba(0, 200, 81, 0.3);
|
||
}
|
||
|
||
.status-pending {
|
||
background: rgba(255, 200, 0, 0.15);
|
||
color: #FFC800;
|
||
border: 1px solid rgba(255, 200, 0, 0.3);
|
||
}
|
||
|
||
.status-closed {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
color: var(--text-secondary);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.pnl-positive {
|
||
background: linear-gradient(135deg, #00C851 0%, #00E676 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.pnl-negative {
|
||
background: linear-gradient(135deg, #FF4444 0%, #FF6B6B 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* === BUTTONS === */
|
||
.close-btn {
|
||
padding: 6px 16px;
|
||
background: rgba(26, 31, 58, 0.6);
|
||
border: 1px solid rgba(0, 240, 255, 0.3);
|
||
color: var(--accent);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
border-radius: 12px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background: rgba(0, 240, 255, 0.15);
|
||
border-color: rgba(0, 240, 255, 0.6);
|
||
box-shadow: 0 0 16px rgba(0, 240, 255, 0.3);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
/* === SWITCH === */
|
||
.switch-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.switch-label {
|
||
font-size: 14px;
|
||
color: var(--text-primary);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.switch {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 50px;
|
||
height: 26px;
|
||
}
|
||
|
||
.switch input {
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
.slider {
|
||
position: absolute;
|
||
cursor: pointer;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(68, 68, 68, 0.6);
|
||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||
border-radius: 26px;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.slider:before {
|
||
position: absolute;
|
||
content: "";
|
||
height: 18px;
|
||
width: 18px;
|
||
left: 4px;
|
||
bottom: 4px;
|
||
background-color: white;
|
||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||
border-radius: 50%;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
input:checked + .slider {
|
||
background: linear-gradient(135deg, #00C851 0%, #00E676 100%);
|
||
box-shadow: 0 0 16px rgba(0, 200, 81, 0.4);
|
||
}
|
||
|
||
input:checked + .slider:before {
|
||
transform: translateX(24px);
|
||
}
|
||
|
||
.switch-disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
min-width: 60px;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.status-text.enabled {
|
||
color: #00C851;
|
||
text-shadow: 0 0 8px rgba(0, 200, 81, 0.6);
|
||
}
|
||
|
||
.status-text.disabled {
|
||
color: #FF4444;
|
||
}
|
||
|
||
/* === EMPTY & LOADING STATES === */
|
||
.empty-state {
|
||
padding: 80px 20px;
|
||
text-align: center;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.empty-state svg {
|
||
width: 72px;
|
||
height: 72px;
|
||
margin-bottom: 20px;
|
||
opacity: 0.3;
|
||
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.2));
|
||
}
|
||
|
||
.empty-state p {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.loading-state {
|
||
padding: 60px;
|
||
text-align: center;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.spinner {
|
||
display: inline-block;
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 3px solid rgba(255, 255, 255, 0.1);
|
||
border-top-color: var(--accent);
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* === RESPONSIVE === */
|
||
@media (max-width: 768px) {
|
||
.trading-container {
|
||
min-width: 100%;
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.account-info {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.tabs {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.tab {
|
||
padding: 12px 20px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.trading-title {
|
||
font-size: 22px;
|
||
}
|
||
|
||
.account-value {
|
||
font-size: 24px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<div class="trading-page">
|
||
<div class="trading-container">
|
||
<div class="sticky-header">
|
||
<div class="trading-header">
|
||
<div class="trading-title">
|
||
实盘交易
|
||
<span class="real-badge">LIVE</span>
|
||
</div>
|
||
<div style="display: flex; align-items: center; gap: 16px;">
|
||
<!-- 自动交易开关 -->
|
||
<div class="switch-container" v-if="apiConfigured">
|
||
<span class="switch-label">自动交易</span>
|
||
<label class="switch" :class="{ 'switch-disabled': !serviceEnabled }">
|
||
<input type="checkbox"
|
||
v-model="autoTradingEnabled"
|
||
@change="toggleAutoTrading"
|
||
:disabled="!serviceEnabled || switching">
|
||
<span class="slider"></span>
|
||
</label>
|
||
<span class="status-text"
|
||
:class="{ enabled: autoTradingEnabled, disabled: !autoTradingEnabled }">
|
||
{{ autoTradingEnabled ? '已启用' : '已禁用' }}
|
||
</span>
|
||
</div>
|
||
<button class="refresh-btn" @click="refreshData">
|
||
刷新
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 警告横幅 - 只在API未配置时显示 -->
|
||
<div class="warning-banner" v-if="!apiConfigured">
|
||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||
</svg>
|
||
<div class="warning-banner-text">
|
||
Bitget API 密钥未配置,请在配置文件中设置 BITGET_API_KEY 和 BITGET_API_SECRET
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 实盘交易未启用提示 -->
|
||
<div class="warning-banner" v-else-if="!serviceEnabled && apiConfigured" style="background: rgba(255, 200, 0, 0.1); border-color: #ffc800;">
|
||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" style="color: #ffc800;">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<div class="warning-banner-text" style="color: #ffc800;">
|
||
实盘交易服务未启用(REAL_TRADING_ENABLED=false),仅可查看账户数据,无法执行交易
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 账户信息 - API 配置后即可显示 -->
|
||
<div class="account-info" v-if="apiConfigured">
|
||
<div class="account-card">
|
||
<div class="account-label">账户余额</div>
|
||
<div class="account-value">${{ account.current_balance ? account.current_balance.toLocaleString() : '0' }}</div>
|
||
<div class="account-sub">可用: ${{ account.available ? account.available.toLocaleString() : '0' }}</div>
|
||
</div>
|
||
<div class="account-card">
|
||
<div class="account-label">已用保证金</div>
|
||
<div class="account-value">${{ account.used_margin ? account.used_margin.toLocaleString() : '0' }}</div>
|
||
</div>
|
||
<div class="account-card">
|
||
<div class="account-label">持仓价值</div>
|
||
<div class="account-value">${{ account.total_position_value ? account.total_position_value.toLocaleString() : '0' }}</div>
|
||
</div>
|
||
<div class="account-card" v-if="stats">
|
||
<div class="account-label">总盈亏</div>
|
||
<div class="account-value" :class="stats.total_pnl >= 0 ? 'positive' : 'negative'">
|
||
${{ stats.total_pnl ? stats.total_pnl.toLocaleString() : '0' }}
|
||
</div>
|
||
<div class="account-sub">{{ stats.total_trades || 0 }} 笔交易</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 标签页 -->
|
||
<div class="tabs">
|
||
<button
|
||
class="tab"
|
||
:class="{ active: currentTab === 'positions' }"
|
||
@click="switchTab('positions')">
|
||
交易所持仓 ({{ exchangePositions.length }})
|
||
</button>
|
||
<button
|
||
class="tab"
|
||
:class="{ active: currentTab === 'orders' }"
|
||
@click="switchTab('orders')">
|
||
历史订单 ({{ orderHistory.length }})
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 订单列表 -->
|
||
<div class="orders-table-container">
|
||
<div v-if="loading" class="loading-state">
|
||
<div class="spinner"></div>
|
||
<p style="margin-top: 16px; font-size: 14px;">加载中...</p>
|
||
</div>
|
||
|
||
<div v-else-if="currentTab === 'positions' && exchangePositions.length === 0" class="empty-state">
|
||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||
</svg>
|
||
<p>暂无持仓</p>
|
||
</div>
|
||
|
||
<div v-else-if="currentTab === 'orders' && orderHistory.length === 0" class="empty-state">
|
||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||
</svg>
|
||
<p>暂无历史订单</p>
|
||
</div>
|
||
|
||
<table class="orders-table" v-else>
|
||
<thead>
|
||
<tr v-if="currentTab === 'positions'">
|
||
<th>交易对</th>
|
||
<th>方向</th>
|
||
<th>持仓量</th>
|
||
<th>入场价</th>
|
||
<th>标记价</th>
|
||
<th>杠杆</th>
|
||
<th>保证金</th>
|
||
<th>未实现盈亏</th>
|
||
<th>盈亏比例</th>
|
||
<th>强平价格</th>
|
||
</tr>
|
||
<tr v-else>
|
||
<th>交易对</th>
|
||
<th>方向</th>
|
||
<th>类型</th>
|
||
<th>价格</th>
|
||
<th>数量</th>
|
||
<th>成交数量</th>
|
||
<th>状态</th>
|
||
<th>时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<!-- 交易所持仓表格 -->
|
||
<template v-if="currentTab === 'positions'">
|
||
<tr v-for="pos in displayOrders" :key="pos.id || pos.symbol">
|
||
<td><strong>{{ formatSymbol(pos.symbol) }}</strong></td>
|
||
<td>
|
||
<span class="side-badge" :class="pos.side === 'long' ? 'side-long' : 'side-short'">
|
||
{{ pos.side === 'long' ? '做多' : '做空' }}
|
||
</span>
|
||
</td>
|
||
<td>{{ pos.contracts || '0' }}</td>
|
||
<td>${{ pos.entryPrice ? parseFloat(pos.entryPrice).toLocaleString() : '-' }}</td>
|
||
<td>${{ pos.markPrice ? parseFloat(pos.markPrice).toLocaleString() : '-' }}</td>
|
||
<td>{{ pos.leverage }}x</td>
|
||
<td>${{ pos.initialMargin ? parseFloat(pos.initialMargin).toFixed(2) : '-' }}</td>
|
||
<td :class="parseFloat(pos.unrealizedPnl || 0) >= 0 ? 'pnl-positive' : 'pnl-negative'">
|
||
{{ parseFloat(pos.unrealizedPnl || 0) >= 0 ? '+' : '' }}${{ parseFloat(pos.unrealizedPnl || 0).toFixed(2) }}
|
||
</td>
|
||
<td :class="(pos.percentage || 0) >= 0 ? 'pnl-positive' : 'pnl-negative'">
|
||
{{ (pos.percentage || 0).toFixed(2) }}%
|
||
</td>
|
||
<td>${{ pos.liquidationPrice ? parseFloat(pos.liquidationPrice).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '-' }}</td>
|
||
</tr>
|
||
</template>
|
||
<!-- 历史订单表格 -->
|
||
<template v-else>
|
||
<tr v-for="order in displayOrders" :key="order.id">
|
||
<td><strong>{{ formatSymbol(order.symbol) }}</strong></td>
|
||
<td>
|
||
<span class="side-badge" :class="order.side === 'buy' ? 'side-long' : 'side-short'">
|
||
{{ order.side === 'buy' ? '做多' : '做空' }}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<span class="status-badge" :class="{
|
||
'status-open': order.status === 'open',
|
||
'status-pending': order.status === 'pending' || order.status === 'partially_filled',
|
||
'status-closed': order.status === 'closed' || order.status === 'filled'
|
||
}">
|
||
{{ formatOrderStatus(order.status) }}
|
||
</span>
|
||
</td>
|
||
<td>${{ order.price ? parseFloat(order.price).toLocaleString() : '-' }}</td>
|
||
<td>{{ order.amount || order.filled || '0' }}</td>
|
||
<td>{{ order.filled || '0' }}</td>
|
||
<td>{{ order.datetime ? formatTime(order.datetime) : '-' }}</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||
<script>
|
||
console.log('[DEBUG] Vue global:', typeof Vue);
|
||
console.log('[DEBUG] Vue.createApp:', typeof Vue?.createApp);
|
||
console.log('[DEBUG] axios:', typeof axios);
|
||
|
||
const { createApp } = Vue;
|
||
|
||
createApp({
|
||
data() {
|
||
return {
|
||
currentTab: 'positions',
|
||
loading: false,
|
||
serviceEnabled: false,
|
||
apiConfigured: false,
|
||
useTestnet: true,
|
||
autoTradingEnabled: false,
|
||
switching: false,
|
||
account: {
|
||
current_balance: 0,
|
||
available: 0,
|
||
used_margin: 0,
|
||
total_position_value: 0
|
||
},
|
||
orderHistory: [],
|
||
exchangePositions: [],
|
||
autoRefreshInterval: null
|
||
};
|
||
},
|
||
computed: {
|
||
displayOrders() {
|
||
if (this.currentTab === 'orders') return this.orderHistory;
|
||
if (this.currentTab === 'positions') return this.exchangePositions;
|
||
return [];
|
||
}
|
||
},
|
||
methods: {
|
||
async switchTab(tab) {
|
||
if (this.currentTab === tab) return; // 已经是当前标签,不重新加载
|
||
this.currentTab = tab;
|
||
|
||
// 切换标签时加载对应数据
|
||
this.loading = true;
|
||
try {
|
||
if (tab === 'orders') {
|
||
await this.fetchOrderHistory();
|
||
}
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
async refreshData() {
|
||
console.log('[refreshData] 开始刷新数据...');
|
||
this.loading = true;
|
||
try {
|
||
await Promise.all([
|
||
this.fetchServiceStatus(),
|
||
this.fetchAccountStatus(),
|
||
this.fetchExchangePositions()
|
||
]);
|
||
|
||
// 根据当前标签页获取对应数据
|
||
if (this.currentTab === 'orders') {
|
||
await this.fetchOrderHistory();
|
||
}
|
||
console.log('[refreshData] 数据刷新完成');
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
async fetchServiceStatus() {
|
||
try {
|
||
console.log('[fetchServiceStatus] 开始请求服务状态...');
|
||
const response = await axios.get('/api/real-trading/status');
|
||
console.log('[fetchServiceStatus] 响应:', response.data);
|
||
if (response.data.success) {
|
||
const status = response.data.status;
|
||
this.serviceEnabled = status.enabled;
|
||
this.apiConfigured = status.api_configured;
|
||
this.useTestnet = status.use_testnet;
|
||
// 获取自动交易状态
|
||
this.autoTradingEnabled = status.auto_trading_enabled || false;
|
||
if (status.account) {
|
||
this.account = status.account;
|
||
}
|
||
console.log('[fetchServiceStatus] 状态已更新:', {
|
||
serviceEnabled: this.serviceEnabled,
|
||
apiConfigured: this.apiConfigured,
|
||
autoTradingEnabled: this.autoTradingEnabled,
|
||
account: this.account
|
||
});
|
||
} else {
|
||
console.error('[fetchServiceStatus] API 返回失败:', response.data.message);
|
||
}
|
||
} catch (error) {
|
||
console.error('[fetchServiceStatus] 获取服务状态失败:', error);
|
||
// 如果是网络错误,显示提示
|
||
if (error.code === 'ERR_NETWORK') {
|
||
console.error('[fetchServiceStatus] 网络错误,请检查后端服务是否启动');
|
||
}
|
||
}
|
||
},
|
||
|
||
async toggleAutoTrading() {
|
||
if (this.switching) return;
|
||
|
||
this.switching = true;
|
||
try {
|
||
const response = await axios.post('/api/real-trading/auto-trading', null, {
|
||
params: { enabled: this.autoTradingEnabled }
|
||
});
|
||
|
||
if (response.data.success) {
|
||
// 刷新状态
|
||
await this.fetchServiceStatus();
|
||
alert(response.data.message);
|
||
} else {
|
||
// 恢复原状态
|
||
this.autoTradingEnabled = !this.autoTradingEnabled;
|
||
alert('设置失败: ' + (response.data.message || '未知错误'));
|
||
}
|
||
} catch (error) {
|
||
console.error('设置自动交易失败:', error);
|
||
// 恢复原状态
|
||
this.autoTradingEnabled = !this.autoTradingEnabled;
|
||
alert('设置失败: ' + (error.response?.data?.detail || error.message));
|
||
} finally {
|
||
this.switching = false;
|
||
}
|
||
},
|
||
|
||
async fetchAccountStatus() {
|
||
try {
|
||
console.log('[fetchAccountStatus] 开始请求账户状态...');
|
||
const response = await axios.get('/api/real-trading/account');
|
||
console.log('[fetchAccountStatus] 响应:', response.data);
|
||
if (response.data.success) {
|
||
this.account = response.data.account;
|
||
} else {
|
||
console.error('[fetchAccountStatus] API 返回失败:', response.data.message);
|
||
}
|
||
} catch (error) {
|
||
console.error('[fetchAccountStatus] 获取账户状态失败:', error);
|
||
if (error.response?.status === 500) {
|
||
console.error('[fetchAccountStatus] 服务器内部错误,请检查后端日志');
|
||
} else if (error.code === 'ERR_NETWORK') {
|
||
console.error('[fetchAccountStatus] 网络错误,请检查后端服务是否启动');
|
||
}
|
||
}
|
||
},
|
||
|
||
async fetchTradeHistory() {
|
||
try {
|
||
// 获取成交记录(包含盈亏)
|
||
const response = await axios.get('/api/real-trading/orders?status=trades&limit=100');
|
||
if (response.data.success) {
|
||
this.tradeHistory = response.data.orders;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取成交记录失败:', error);
|
||
}
|
||
},
|
||
|
||
async fetchOrderHistory() {
|
||
try {
|
||
console.log('[fetchOrderHistory] 开始请求历史订单...');
|
||
// 获取历史订单
|
||
const response = await axios.get('/api/real-trading/orders?status=orders&limit=50');
|
||
console.log('[fetchOrderHistory] 响应:', response.data);
|
||
if (response.data.success) {
|
||
this.orderHistory = response.data.orders;
|
||
} else {
|
||
console.error('[fetchOrderHistory] API 返回失败:', response.data.message);
|
||
this.orderHistory = [];
|
||
}
|
||
} catch (error) {
|
||
console.error('[fetchOrderHistory] 获取历史订单失败:', error);
|
||
this.orderHistory = [];
|
||
if (error.code === 'ERR_NETWORK') {
|
||
console.error('[fetchOrderHistory] 网络错误,请检查后端服务是否启动');
|
||
}
|
||
}
|
||
},
|
||
|
||
async fetchExchangePositions() {
|
||
try {
|
||
console.log('[fetchExchangePositions] 开始请求交易所持仓...');
|
||
const response = await axios.get('/api/real-trading/positions');
|
||
console.log('[fetchExchangePositions] 响应:', response.data);
|
||
if (response.data.success) {
|
||
this.exchangePositions = response.data.positions;
|
||
} else {
|
||
console.error('[fetchExchangePositions] API 返回失败:', response.data.message);
|
||
this.exchangePositions = [];
|
||
}
|
||
} catch (error) {
|
||
console.error('[fetchExchangePositions] 获取交易所持仓失败:', error);
|
||
this.exchangePositions = [];
|
||
if (error.code === 'ERR_NETWORK') {
|
||
console.error('[fetchExchangePositions] 网络错误,请检查后端服务是否启动');
|
||
}
|
||
}
|
||
},
|
||
|
||
formatTime(timeStr) {
|
||
if (!timeStr) return '-';
|
||
const date = new Date(timeStr);
|
||
return date.toLocaleString('zh-CN', {
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
},
|
||
|
||
formatStatus(status) {
|
||
const map = {
|
||
'open': '持仓中',
|
||
'pending': '挂单中',
|
||
'closed': '已平仓',
|
||
'cancelled': '已取消'
|
||
};
|
||
return map[status] || status;
|
||
},
|
||
|
||
formatSymbol(symbol) {
|
||
// 将 CCXT 格式的交易对转换为简洁格式
|
||
// 例如:BTC/USDT:USDT -> BTCUSDT
|
||
if (!symbol) return '-';
|
||
return symbol.replace('/', '').replace(':', '');
|
||
},
|
||
|
||
formatOrderStatus(status) {
|
||
const map = {
|
||
'open': '挂单中',
|
||
'closed': '已成交',
|
||
'canceled': '已取消',
|
||
'cancelled': '已取消',
|
||
'filled': '已成交',
|
||
'partially_filled': '部分成交',
|
||
'rejected': '已拒绝',
|
||
'expired': '已过期'
|
||
};
|
||
return map[status] || status;
|
||
},
|
||
|
||
getFeeClass(order) {
|
||
const fee = parseFloat(order.fee || 0);
|
||
if (fee === 0) return '';
|
||
// 如果是 taker (fee > 0),显示为负(花费)
|
||
// 如果是 maker (fee < 0),显示为正(返还)
|
||
return fee > 0 ? 'fee-negative' : 'fee-positive';
|
||
}
|
||
},
|
||
mounted() {
|
||
console.log('[Vue] 应用已挂载,开始加载数据...');
|
||
console.log('[Vue] 当前状态:', {
|
||
currentTab: this.currentTab,
|
||
loading: this.loading,
|
||
serviceEnabled: this.serviceEnabled,
|
||
apiConfigured: this.apiConfigured,
|
||
autoTradingEnabled: this.autoTradingEnabled
|
||
});
|
||
this.refreshData();
|
||
// 不自动刷新,用户可以手动点击刷新按钮
|
||
},
|
||
beforeUnmount() {
|
||
if (this.autoRefreshInterval) {
|
||
clearInterval(this.autoRefreshInterval);
|
||
}
|
||
}
|
||
}).mount('#app');
|
||
</script>
|
||
</body>
|
||
</html>
|