This commit is contained in:
aaron 2026-03-30 01:38:16 +08:00
parent df55f2ff37
commit 37b1dc682d

View File

@ -266,6 +266,66 @@
.admin-menu-item:last-child {
border-radius: 0 0 var(--radius-md) var(--radius-md);
}
.pending-stack {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.pending-main {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
line-height: 1.4;
}
.pending-sub {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
}
.pending-sub.text-success,
.pending-sub.text-error {
font-weight: 600;
}
.pending-chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.pending-chip {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
background: var(--bg-secondary);
border: 1px solid var(--border);
font-size: 11px;
color: var(--text-secondary);
line-height: 1.4;
white-space: nowrap;
}
.pending-reason {
max-width: 280px;
white-space: normal;
word-break: break-word;
}
.pending-signal {
min-width: 140px;
}
@media (max-width: 1200px) {
.pending-reason {
max-width: 220px;
}
}
</style>
</head>
<body>
@ -436,11 +496,12 @@
<tr>
<th>交易对</th>
<th>方向</th>
<th>数量</th>
<th>挂单价</th>
<th>杠杆</th>
<th>止损</th>
<th>止盈</th>
<th>规模</th>
<th>挂单信息</th>
<th>风控目标</th>
<th>仓位占用</th>
<th>信号</th>
<th>理由</th>
<th>时间</th>
<th>操作</th>
</tr>
@ -453,11 +514,51 @@
{{ 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>
<div class="pending-stack">
<div class="pending-main">{{ formatNumber(order.quantity, 4) }}</div>
<div class="pending-sub">杠杆 {{ order.leverage || 0 }}x</div>
</div>
</td>
<td>
<div class="pending-stack">
<div class="pending-main">{{ formatCurrency(order.display_entry_price) }}</div>
<div class="pending-sub">现价 {{ formatCurrency(order.current_price) }}</div>
<div class="pending-sub" :class="order.distance_class">{{ order.distance_text }}</div>
</div>
</td>
<td>
<div class="pending-stack">
<div class="pending-main">{{ formatCurrency(order.stop_loss) }}</div>
<div class="pending-sub text-error">止损 {{ formatOptionalSignedPercent(order.stop_loss_percent) }}</div>
<div class="pending-main">{{ formatCurrency(order.take_profit) }}</div>
<div class="pending-sub text-success">止盈 {{ formatOptionalSignedPercent(order.take_profit_percent) }}</div>
<div class="pending-sub">盈亏比 {{ order.risk_reward_text }}</div>
</div>
</td>
<td>
<div class="pending-stack">
<div class="pending-main">{{ formatCurrency(order.margin) }}</div>
<div class="pending-sub">保证金</div>
<div class="pending-main">{{ formatCurrency(order.expected_position_value) }}</div>
<div class="pending-sub">名义仓位</div>
</div>
</td>
<td>
<div class="pending-stack pending-signal">
<div class="pending-main">{{ order.signal_grade_text }}</div>
<div class="pending-chip-row">
<span v-if="order.signal_type_text" class="pending-chip">{{ order.signal_type_text }}</span>
<span v-if="order.confidence_text" class="pending-chip">{{ order.confidence_text }}</span>
<span v-if="order.entry_type_text" class="pending-chip">{{ order.entry_type_text }}</span>
</div>
</div>
</td>
<td>
<div class="pending-stack pending-reason">
<div class="pending-main">{{ order.reason_preview }}</div>
</div>
</td>
<td>{{ formatTime(order.created_at) }}</td>
<td>
<button class="btn btn-danger btn-small" @click="cancelOrder(order)">撤单</button>
@ -678,7 +779,35 @@
});
},
pendingOrders() {
return this.orders.filter(order => order.status === 'pending');
return this.orders
.filter(order => order.status === 'pending')
.map(order => {
const entryPrice = Number(order.entry_price || 0);
const currentPrice = this.resolveOrderCurrentPrice(order);
const stopLossPercent = this.calculateOrderTargetPercent(order.side, entryPrice, order.stop_loss);
const takeProfitPercent = this.calculateOrderTargetPercent(order.side, entryPrice, order.take_profit);
const margin = Number(order.margin || 0);
const leverage = Number(order.leverage || 0);
return {
...order,
display_entry_price: entryPrice,
current_price: currentPrice || null,
distance_percent: this.calculatePendingDistancePercent(currentPrice, entryPrice),
distance_text: this.getPendingDistanceText(currentPrice, entryPrice),
distance_class: this.getPendingDistanceClass(currentPrice, entryPrice),
stop_loss_percent: stopLossPercent,
take_profit_percent: takeProfitPercent,
risk_reward_ratio: this.calculateRiskRewardRatio(stopLossPercent, takeProfitPercent),
risk_reward_text: this.formatRiskRewardRatio(stopLossPercent, takeProfitPercent),
expected_position_value: margin > 0 && leverage > 0 ? margin * leverage : 0,
signal_grade_text: order.signal_grade || '-',
signal_type_text: this.getSignalTypeText(order.signal_type),
confidence_text: this.formatConfidence(order.confidence),
entry_type_text: this.getEntryTypeText(order.entry_type),
reason_preview: this.getPendingReasonPreview(order)
};
});
},
orderHistory() {
return this.orders.filter(order => {
@ -865,12 +994,25 @@
return `${number >= 0 ? '+' : '-'}${Math.abs(number).toFixed(2)}%`;
},
formatOptionalSignedPercent(value) {
const number = Number(value);
if (!Number.isFinite(number)) return '-';
return this.formatSignedPercent(number);
},
formatNumber(value, digits = 2) {
const number = Number(value);
if (!Number.isFinite(number)) return (0).toFixed(digits);
return number.toFixed(digits);
},
formatConfidence(value) {
const number = Number(value);
if (!Number.isFinite(number) || number <= 0) return '';
const percent = number <= 1 ? number * 100 : number;
return `置信度 ${percent.toFixed(1)}%`;
},
getDisplayEntryPrice(order) {
const price = Number(order.filled_price || order.entry_price || 0);
return Number.isFinite(price) ? price : 0;
@ -914,6 +1056,114 @@
return positionValue * pnlPercent / 100;
},
calculatePendingDistancePercent(currentPrice, entryPrice) {
const current = Number(currentPrice);
const entry = Number(entryPrice);
if (!Number.isFinite(current) || current <= 0 || !Number.isFinite(entry) || entry <= 0) {
return null;
}
return ((entry - current) / current) * 100;
},
getPendingDistanceText(currentPrice, entryPrice) {
const distancePercent = this.calculatePendingDistancePercent(currentPrice, entryPrice);
if (!Number.isFinite(distancePercent)) {
return '等待触发';
}
if (Math.abs(distancePercent) < 0.05) {
return '接近触发';
}
const direction = distancePercent > 0 ? '需上涨触发' : '需回落触发';
return `${direction} ${this.formatPercent(Math.abs(distancePercent))}`;
},
getPendingDistanceClass(currentPrice, entryPrice) {
const distancePercent = this.calculatePendingDistancePercent(currentPrice, entryPrice);
if (!Number.isFinite(distancePercent)) {
return '';
}
if (Math.abs(distancePercent) < 0.35) {
return 'text-success';
}
if (Math.abs(distancePercent) < 1) {
return '';
}
return 'text-error';
},
calculateOrderTargetPercent(side, entryPrice, targetPrice) {
const entry = Number(entryPrice);
const target = Number(targetPrice);
if (!Number.isFinite(entry) || entry <= 0 || !Number.isFinite(target) || target <= 0) {
return null;
}
if (side === 'long') {
return ((target - entry) / entry) * 100;
}
if (side === 'short') {
return ((entry - target) / entry) * 100;
}
return null;
},
calculateRiskRewardRatio(stopLossPercent, takeProfitPercent) {
const stop = Number(stopLossPercent);
const take = Number(takeProfitPercent);
if (!Number.isFinite(stop) || !Number.isFinite(take) || stop >= 0 || take <= 0) {
return null;
}
return take / Math.abs(stop);
},
formatRiskRewardRatio(stopLossPercent, takeProfitPercent) {
const ratio = this.calculateRiskRewardRatio(stopLossPercent, takeProfitPercent);
if (!Number.isFinite(ratio) || ratio <= 0) {
return '-';
}
return `1:${ratio.toFixed(2)}`;
},
formatPercent(value, digits = 2) {
const number = Number(value);
if (!Number.isFinite(number)) return '-';
return `${number.toFixed(digits)}%`;
},
getSignalTypeText(signalType) {
const map = {
short_term: '日内',
medium_term: '中线',
long_term: '趋势',
swing: '波段'
};
return map[signalType] || signalType || '';
},
getEntryTypeText(entryType) {
const map = {
market: '市价',
limit: '限价',
pending: '挂单'
};
return map[entryType] || entryType || '';
},
getPendingReasonPreview(order) {
const reasons = Array.isArray(order.entry_reasons)
? order.entry_reasons.filter(Boolean)
: [];
const baseText = reasons.length > 0 ? reasons[0] : '暂无入场理由';
return baseText.length > 58 ? `${baseText.slice(0, 58)}...` : baseText;
},
getCloseReason(reason) {
const map = {
'manual': '手动',