782 lines
34 KiB
HTML
782 lines
34 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>XClaw AI</title>
|
||
|
||
<!-- Global Styles -->
|
||
<link rel="stylesheet" href="/static/css/style.css">
|
||
|
||
<!-- Fonts -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
|
||
<!-- Page-Specific Styles -->
|
||
<style>
|
||
.paper-badge {
|
||
display: inline-block;
|
||
background: var(--primary);
|
||
color: white;
|
||
padding: 4px 12px;
|
||
border-radius: var(--radius-sm);
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
letter-spacing: 1px;
|
||
margin-left: 12px;
|
||
}
|
||
|
||
.metrics-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||
gap: var(--space-md);
|
||
margin-bottom: var(--space-xl);
|
||
}
|
||
|
||
.stat-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||
gap: var(--space-md);
|
||
margin-bottom: var(--space-lg);
|
||
}
|
||
|
||
.metric-card, .stat-card {
|
||
background: var(--bg-primary);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-md);
|
||
padding: var(--space-lg);
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.metric-card:hover, .stat-card:hover {
|
||
border-color: var(--primary);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.metric-label, .stat-label {
|
||
font-size: var(--font-sm);
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
margin-bottom: var(--space-sm);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.metric-value, .stat-value {
|
||
font-size: var(--font-2xl);
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
margin-bottom: var(--space-xs);
|
||
font-family: 'Inter', monospace;
|
||
}
|
||
|
||
.metric-sub {
|
||
font-size: var(--font-sm);
|
||
color: var(--text-tertiary);
|
||
}
|
||
|
||
.metric-value.positive, .stat-value.positive {
|
||
color: var(--success);
|
||
}
|
||
|
||
.metric-value.negative, .stat-value.negative {
|
||
color: var(--error);
|
||
}
|
||
|
||
.grade-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||
gap: var(--space-lg);
|
||
margin-top: var(--space-xl);
|
||
}
|
||
|
||
.grade-card {
|
||
background: var(--bg-primary);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-md);
|
||
padding: var(--space-lg);
|
||
}
|
||
|
||
.grade-card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: var(--space-md);
|
||
padding-bottom: var(--space-md);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.grade-card-title {
|
||
font-size: var(--font-md);
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.grade-card-stats {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-sm);
|
||
}
|
||
|
||
.grade-stat-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: var(--space-sm) 0;
|
||
}
|
||
|
||
.grade-stat-label {
|
||
font-size: var(--font-sm);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.grade-stat-value {
|
||
font-size: var(--font-base);
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.grade-stat-value.positive {
|
||
color: var(--success);
|
||
}
|
||
|
||
.grade-stat-value.negative {
|
||
color: var(--error);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.metrics-grid, .stat-grid, .grade-stats {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Admin Dropdown */
|
||
.admin-dropdown {
|
||
position: relative;
|
||
}
|
||
|
||
.admin-menu {
|
||
position: absolute;
|
||
top: 100%;
|
||
right: 0;
|
||
margin-top: 8px;
|
||
background: var(--bg-primary);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-md);
|
||
box-shadow: var(--shadow-lg);
|
||
min-width: 200px;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.admin-menu-item {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
background: none;
|
||
border: none;
|
||
text-align: left;
|
||
font-size: 14px;
|
||
color: var(--text-primary);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.admin-menu-item:hover {
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
.admin-menu-item:first-child {
|
||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||
}
|
||
|
||
.admin-menu-item:last-child {
|
||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<div class="container">
|
||
<!-- Header -->
|
||
<div class="page-header">
|
||
<div>
|
||
<div class="page-title" @click="handleTitleClick" style="cursor: pointer;">
|
||
XClaw
|
||
<span class="paper-badge">AI</span>
|
||
</div>
|
||
<div class="page-subtitle">XClaw AI Auto Trading</div>
|
||
</div>
|
||
<div class="header-actions">
|
||
<button class="btn btn-secondary" @click="refreshData">刷新</button>
|
||
<button class="btn btn-danger" @click="resetAccount" v-if="currentTab === 'positions' && openPositions.length > 0">
|
||
重置账户
|
||
</button>
|
||
<!-- 管理员菜单 -->
|
||
<div class="admin-dropdown" v-if="adminMode">
|
||
<button class="btn btn-secondary" @click="toggleAdminMenu">管理 ▾</button>
|
||
<div class="admin-menu" v-if="showAdminMenu">
|
||
<button class="admin-menu-item" @click="toggleAdminMode">
|
||
🔓 关闭管理员模式
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Account Info -->
|
||
<div class="metrics-grid">
|
||
<div class="metric-card">
|
||
<div class="metric-label">账户余额</div>
|
||
<div class="metric-value">${{ account.current_balance ? account.current_balance.toLocaleString() : '0' }}</div>
|
||
<div class="metric-sub">可用: ${{ account.available_margin ? account.available_margin.toLocaleString() : '0' }}</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-label">持仓价值</div>
|
||
<div class="metric-value">${{ account.total_position_value ? account.total_position_value.toLocaleString() : '0' }}</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-label">已实现盈亏</div>
|
||
<div class="metric-value" :class="stats.total_pnl >= 0 ? 'positive' : 'negative'">
|
||
${{ stats.total_pnl ? stats.total_pnl.toLocaleString() : '0' }}
|
||
</div>
|
||
<div class="metric-sub">{{ stats.total_trades || 0 }} 笔交易</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-label">收益率</div>
|
||
<div class="metric-value" :class="stats.total_pnl_percent >= 0 ? 'positive' : 'negative'">
|
||
{{ stats.total_pnl_percent >= 0 ? '+' : '' }}{{ stats.total_pnl_percent ? stats.total_pnl_percent.toFixed(2) : '0.00' }}%
|
||
</div>
|
||
<div class="metric-sub">初始资金: $10,000</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stats Grid -->
|
||
<div class="stat-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-label">胜率</div>
|
||
<div class="stat-value positive">{{ stats.win_rate ? stats.win_rate.toFixed(1) : '0.0' }}%</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">盈亏比</div>
|
||
<div class="stat-value">{{ stats.profit_factor === Infinity ? '∞' : (stats.profit_factor ? stats.profit_factor.toFixed(2) : '0.00') }}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">最大回撤</div>
|
||
<div class="stat-value" :class="stats.max_drawdown <= 0 ? 'negative' : 'positive'">
|
||
{{ stats.max_drawdown ? stats.max_drawdown.toFixed(2) : '0.00' }}%
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">总杠杆率</div>
|
||
<div class="stat-value">{{ account.current_total_leverage ? account.current_total_leverage.toFixed(1) : '0.0' }}x / {{ account.max_total_leverage || 10 }}x</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tabs -->
|
||
<div class="tabs">
|
||
<button class="tab" :class="{ active: currentTab === 'positions' }" @click="switchTab('positions')">
|
||
当前持仓 ({{ openPositions.length }})
|
||
</button>
|
||
<button class="tab" :class="{ active: currentTab === 'pending' }" @click="switchTab('pending')">
|
||
挂单中 ({{ pendingOrders.length }})
|
||
</button>
|
||
<button class="tab" :class="{ active: currentTab === 'history' }" @click="switchTab('history')">
|
||
历史订单 ({{ orderHistory.length }})
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Content -->
|
||
<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' && openPositions.length === 0" class="empty-state">
|
||
<p>暂无持仓</p>
|
||
</div>
|
||
|
||
<div v-else-if="currentTab === 'pending' && pendingOrders.length === 0" class="empty-state">
|
||
<p>暂无挂单</p>
|
||
</div>
|
||
|
||
<div v-else-if="currentTab === 'history' && orderHistory.length === 0" class="empty-state">
|
||
<p>暂无历史订单</p>
|
||
</div>
|
||
|
||
<div v-else>
|
||
<!-- Open Positions Table -->
|
||
<div v-if="currentTab === 'positions'" class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>交易对</th>
|
||
<th>方向</th>
|
||
<th>数量</th>
|
||
<th>入场价</th>
|
||
<th>当前价</th>
|
||
<th>杠杆</th>
|
||
<th>保证金</th>
|
||
<th>未实现盈亏</th>
|
||
<th>盈亏比例</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="order in openPositions" :key="order.order_id">
|
||
<td><strong>{{ order.symbol }}</strong></td>
|
||
<td>
|
||
<span class="badge" :class="order.side === 'long' ? 'badge-success' : 'badge-error'">
|
||
{{ order.side === 'long' ? '做多' : '做空' }}
|
||
</span>
|
||
</td>
|
||
<td>{{ order.quantity ? order.quantity.toFixed(4) : '0.0000' }}</td>
|
||
<td>{{ order.entry_price ? '$' + order.entry_price.toFixed(2) : '$0.00' }}</td>
|
||
<td>{{ order.current_price ? '$' + order.current_price.toFixed(2) : '-' }}</td>
|
||
<td>{{ order.leverage || 0 }}x</td>
|
||
<td>{{ order.margin ? '$' + order.margin.toFixed(2) : '$0.00' }}</td>
|
||
<td :class="order.unrealized_pnl >= 0 ? 'text-success' : 'text-error'">
|
||
{{ order.unrealized_pnl >= 0 ? '+' : '' }}${{ order.unrealized_pnl ? order.unrealized_pnl.toFixed(2) : '0.00' }}
|
||
</td>
|
||
<td :class="order.pnl_percent >= 0 ? 'text-success' : 'text-error'">
|
||
{{ order.pnl_percent >= 0 ? '+' : '' }}{{ order.pnl_percent ? order.pnl_percent.toFixed(2) : '0.00' }}%
|
||
</td>
|
||
<td>
|
||
<button class="btn btn-danger btn-small" @click="closeOrder(order)">平仓</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Pending Orders Table -->
|
||
<div v-else-if="currentTab === 'pending'" class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>交易对</th>
|
||
<th>方向</th>
|
||
<th>数量</th>
|
||
<th>挂单价</th>
|
||
<th>杠杆</th>
|
||
<th>止损</th>
|
||
<th>止盈</th>
|
||
<th>时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="order in pendingOrders" :key="order.order_id">
|
||
<td><strong>{{ order.symbol }}</strong></td>
|
||
<td>
|
||
<span class="badge" :class="order.side === 'long' ? 'badge-success' : 'badge-error'">
|
||
{{ order.side === 'long' ? '做多' : '做空' }}
|
||
</span>
|
||
</td>
|
||
<td>{{ order.quantity ? order.quantity.toFixed(4) : '0.0000' }}</td>
|
||
<td>{{ order.entry_price ? '$' + order.entry_price.toFixed(2) : '$0.00' }}</td>
|
||
<td>{{ order.leverage || 0 }}x</td>
|
||
<td>{{ order.stop_loss ? '$' + order.stop_loss.toFixed(2) : '-' }}</td>
|
||
<td>{{ order.take_profit ? '$' + order.take_profit.toFixed(2) : '-' }}</td>
|
||
<td>{{ formatTime(order.created_at) }}</td>
|
||
<td>
|
||
<button class="btn btn-danger btn-small" @click="cancelOrder(order)">撤单</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Order History Table -->
|
||
<div v-else class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>交易对</th>
|
||
<th>方向</th>
|
||
<th>数量</th>
|
||
<th>入场价</th>
|
||
<th>出场价</th>
|
||
<th>实际收益</th>
|
||
<th>收益率</th>
|
||
<th>状态</th>
|
||
<th>时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="order in orderHistory" :key="order.order_id">
|
||
<td><strong>{{ order.symbol }}</strong></td>
|
||
<td>
|
||
<span class="badge" :class="order.side === 'long' ? 'badge-success' : 'badge-error'">
|
||
{{ order.side === 'long' ? '做多' : '做空' }}
|
||
</span>
|
||
</td>
|
||
<td>{{ order.quantity ? order.quantity.toFixed(4) : '0.0000' }}</td>
|
||
<td>{{ order.entry_price ? '$' + order.entry_price.toFixed(2) : '$0.00' }}</td>
|
||
<td>{{ order.exit_price ? '$' + order.exit_price.toFixed(2) : '$0.00' }}</td>
|
||
<td :class="order.pnl_amount >= 0 ? 'text-success' : 'text-error'">
|
||
{{ order.pnl_amount >= 0 ? '+' : '' }}${{ order.pnl_amount ? order.pnl_amount.toFixed(2) : '0.00' }}
|
||
</td>
|
||
<td :class="order.pnl_percent >= 0 ? 'text-success' : 'text-error'">
|
||
{{ order.pnl_percent >= 0 ? '+' : '' }}{{ order.pnl_percent ? order.pnl_percent.toFixed(2) : '0.00' }}%
|
||
</td>
|
||
<td>
|
||
<span class="badge" :class="getStatusBadgeClass(order.status)">
|
||
{{ getStatusText(order.status) }}
|
||
</span>
|
||
</td>
|
||
<td>{{ formatTime(order.closed_at) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Detailed Stats (Only show in history tab) -->
|
||
<div v-if="currentTab === 'history' && orderHistory.length > 0" class="grade-stats">
|
||
<div class="grade-card">
|
||
<div class="grade-card-header">
|
||
<span class="grade-card-title">交易详情</span>
|
||
</div>
|
||
<div class="grade-card-stats">
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">总交易数</span>
|
||
<span class="grade-stat-value">{{ stats.total_trades || 0 }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">盈利交易</span>
|
||
<span class="grade-stat-value positive">{{ stats.winning_trades || 0 }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">亏损交易</span>
|
||
<span class="grade-stat-value negative">{{ stats.losing_trades || 0 }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">最佳交易</span>
|
||
<span class="grade-stat-value positive">{{ stats.best_trade ? stats.best_trade.toFixed(2) : '0.00' }}%</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">最差交易</span>
|
||
<span class="grade-stat-value negative">{{ stats.worst_trade ? stats.worst_trade.toFixed(2) : '0.00' }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grade-card">
|
||
<div class="grade-card-header">
|
||
<span class="grade-card-title">收益分析</span>
|
||
</div>
|
||
<div class="grade-card-stats">
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">平均盈利</span>
|
||
<span class="grade-stat-value positive">${{ stats.average_win ? stats.average_win.toFixed(2) : '0.00' }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">平均亏损</span>
|
||
<span class="grade-stat-value negative">${{ stats.average_loss ? stats.average_loss.toFixed(2) : '0.00' }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">收益率</span>
|
||
<span class="grade-stat-value" :class="stats.return_percent >= 0 ? 'positive' : 'negative'">
|
||
{{ stats.return_percent >= 0 ? '+' : '' }}{{ stats.return_percent ? stats.return_percent.toFixed(2) : '0.00' }}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||
<script>
|
||
const { createApp } = Vue;
|
||
|
||
createApp({
|
||
data() {
|
||
return {
|
||
currentTab: 'positions',
|
||
loading: false,
|
||
sendingReport: false,
|
||
stats: {
|
||
total_trades: 0,
|
||
winning_trades: 0,
|
||
losing_trades: 0,
|
||
win_rate: 0,
|
||
total_pnl: 0,
|
||
total_pnl_percent: 0,
|
||
average_win: 0,
|
||
average_loss: 0,
|
||
profit_factor: 0,
|
||
max_drawdown: 0,
|
||
best_trade: 0,
|
||
worst_trade: 0,
|
||
return_percent: 0
|
||
},
|
||
account: {
|
||
initial_balance: 10000,
|
||
current_balance: 10000,
|
||
used_margin: 0,
|
||
available_margin: 10000,
|
||
leverage: 10,
|
||
margin_per_order: 1000,
|
||
active_orders: 0,
|
||
max_orders: 10,
|
||
available_orders: 10,
|
||
total_position_value: 0,
|
||
margin_ratio: 0,
|
||
realized_pnl: 0,
|
||
current_total_leverage: 0,
|
||
max_total_leverage: 10
|
||
},
|
||
orders: [],
|
||
adminMode: false,
|
||
showAdminMenu: false,
|
||
adminPassword: '223388',
|
||
titleClickCount: 0,
|
||
titleClickTimer: null
|
||
};
|
||
},
|
||
computed: {
|
||
openPositions() {
|
||
return this.orders.filter(o => o.status === 'open');
|
||
},
|
||
pendingOrders() {
|
||
return this.orders.filter(o => o.status === 'pending');
|
||
},
|
||
orderHistory() {
|
||
// 包含所有已关闭的订单:closed_tp, closed_sl, closed_be, closed_ts, closed_manual, cancelled
|
||
return this.orders.filter(o =>
|
||
o.status.startsWith('closed') || o.status === 'cancelled'
|
||
);
|
||
}
|
||
},
|
||
methods: {
|
||
async switchTab(tab) {
|
||
this.currentTab = tab;
|
||
},
|
||
|
||
async refreshData() {
|
||
this.loading = true;
|
||
try {
|
||
await Promise.all([
|
||
this.fetchAccountStatus(),
|
||
this.fetchStatistics(),
|
||
this.fetchOrders()
|
||
]);
|
||
} catch (e) {
|
||
console.error('刷新数据失败:', e);
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
async fetchAccountStatus() {
|
||
try {
|
||
const response = await axios.get('/api/trading/account');
|
||
if (response.data.success) {
|
||
this.account = response.data.account;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取账户状态失败:', error);
|
||
}
|
||
},
|
||
|
||
async fetchStatistics() {
|
||
try {
|
||
const response = await axios.get('/api/trading/statistics');
|
||
if (response.data.success) {
|
||
this.stats = response.data.statistics;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取统计数据失败:', error);
|
||
}
|
||
},
|
||
|
||
async fetchOrders() {
|
||
try {
|
||
const response = await axios.get('/api/trading/orders');
|
||
console.log('API Response:', response.data);
|
||
if (response.data.success) {
|
||
this.orders = response.data.orders || [];
|
||
console.log('Orders loaded:', this.orders.length);
|
||
console.log('Orders data:', this.orders);
|
||
console.log('Open positions:', this.orders.filter(o => o.status === 'open'));
|
||
console.log('Pending orders:', this.orders.filter(o => o.status === 'pending'));
|
||
console.log('Order history:', this.orders.filter(o => o.status === 'closed'));
|
||
}
|
||
} catch (error) {
|
||
console.error('获取订单失败:', error);
|
||
}
|
||
},
|
||
|
||
async closeOrder(order) {
|
||
if (!confirm('确定要平仓吗?')) return;
|
||
|
||
try {
|
||
const response = await axios.post(`/api/trading/orders/${order.order_id}/close`);
|
||
if (response.data.success) {
|
||
await this.refreshData();
|
||
alert('平仓成功');
|
||
} else {
|
||
alert('平仓失败: ' + (response.data.message || '未知错误'));
|
||
}
|
||
} catch (error) {
|
||
console.error('平仓失败:', error);
|
||
alert('平仓失败: ' + (error.response?.data?.detail || error.message));
|
||
}
|
||
},
|
||
|
||
async cancelOrder(order) {
|
||
if (!confirm('确定要撤单吗?')) return;
|
||
|
||
try {
|
||
const response = await axios.post(`/api/trading/orders/${order.order_id}/cancel`);
|
||
if (response.data.success) {
|
||
await this.refreshData();
|
||
alert('撤单成功');
|
||
} else {
|
||
alert('撤单失败: ' + (response.data.message || '未知错误'));
|
||
}
|
||
} catch (error) {
|
||
console.error('撤单失败:', error);
|
||
alert('撤单失败: ' + (error.response?.data?.detail || error.message));
|
||
}
|
||
},
|
||
|
||
async resetAccount() {
|
||
if (!confirm('确定要重置账户吗?这将清除所有持仓和订单!')) return;
|
||
|
||
try {
|
||
const response = await axios.post('/api/trading/account/reset');
|
||
if (response.data.success) {
|
||
await this.refreshData();
|
||
alert('账户重置成功');
|
||
} else {
|
||
alert('重置失败: ' + (response.data.message || '未知错误'));
|
||
}
|
||
} catch (error) {
|
||
console.error('重置账户失败:', error);
|
||
alert('重置失败: ' + (error.response?.data?.detail || error.message));
|
||
}
|
||
},
|
||
|
||
async sendReport() {
|
||
this.sendingReport = true;
|
||
try {
|
||
const response = await axios.post('/api/trading/report?hours=4&send_telegram=true');
|
||
if (response.data.success) {
|
||
alert('报告已发送');
|
||
} else {
|
||
alert('发送失败: ' + (response.data.message || '未知错误'));
|
||
}
|
||
} catch (error) {
|
||
console.error('发送报告失败:', error);
|
||
alert('发送失败: ' + (error.response?.data?.detail || error.message));
|
||
} finally {
|
||
this.sendingReport = false;
|
||
}
|
||
},
|
||
|
||
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'
|
||
});
|
||
},
|
||
|
||
getCloseReason(reason) {
|
||
const map = {
|
||
'manual': '手动',
|
||
'stop_loss': '止损',
|
||
'take_profit': '止盈',
|
||
'timeout': '超时',
|
||
'signal': '信号'
|
||
};
|
||
return map[reason] || reason || '-';
|
||
},
|
||
|
||
handleTitleClick() {
|
||
this.titleClickCount++;
|
||
if (this.titleClickTimer) {
|
||
clearTimeout(this.titleClickTimer);
|
||
}
|
||
this.titleClickTimer = setTimeout(() => {
|
||
this.titleClickCount = 0;
|
||
}, 2000);
|
||
|
||
if (this.titleClickCount === 3) {
|
||
this.titleClickCount = 0;
|
||
this.promptAdminMode();
|
||
}
|
||
},
|
||
|
||
promptAdminMode() {
|
||
if (this.adminMode) {
|
||
this.adminMode = false;
|
||
this.showAdminMenu = false;
|
||
alert('管理员模式已关闭');
|
||
} else {
|
||
const password = prompt('请输入管理员密码:');
|
||
if (password === this.adminPassword) {
|
||
this.adminMode = true;
|
||
alert('管理员模式已开启');
|
||
} else if (password !== null) {
|
||
alert('密码错误');
|
||
}
|
||
}
|
||
},
|
||
|
||
toggleAdminMenu() {
|
||
this.showAdminMenu = !this.showAdminMenu;
|
||
},
|
||
|
||
toggleAdminMode() {
|
||
this.promptAdminMode();
|
||
},
|
||
|
||
getStatusBadgeClass(status) {
|
||
const classMap = {
|
||
'closed_tp': 'badge-success',
|
||
'closed_sl': 'badge-error',
|
||
'closed_be': 'badge-warning',
|
||
'closed_ts': 'badge-success',
|
||
'closed_manual': 'badge-info',
|
||
'cancelled': 'badge-secondary'
|
||
};
|
||
return classMap[status] || 'badge-secondary';
|
||
},
|
||
|
||
getStatusText(status) {
|
||
const textMap = {
|
||
'pending': '挂单中',
|
||
'open': '持仓中',
|
||
'closed_tp': '止盈平仓',
|
||
'closed_sl': '止损平仓',
|
||
'closed_be': '保本平仓',
|
||
'closed_ts': '移动止盈',
|
||
'closed_manual': '手动平仓',
|
||
'cancelled': '已取消'
|
||
};
|
||
return textMap[status] || status;
|
||
}
|
||
},
|
||
mounted() {
|
||
this.refreshData();
|
||
|
||
// 点击外部关闭管理菜单
|
||
document.addEventListener('click', (e) => {
|
||
if (this.showAdminMenu && !e.target.closest('.admin-dropdown')) {
|
||
this.showAdminMenu = false;
|
||
}
|
||
});
|
||
}
|
||
}).mount('#app');
|
||
</script>
|
||
</body>
|
||
</html>
|