a
This commit is contained in:
parent
8bfe9d95da
commit
31486b5026
|
|
@ -41,11 +41,113 @@
|
|||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.snapshot-btn {
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.snapshot-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.guide-link:hover {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 配置快照弹窗 */
|
||||
.snapshot-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.snapshot-modal {
|
||||
width: min(980px, 100%);
|
||||
max-height: 85vh;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.snapshot-modal-header {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.snapshot-modal-header h3 {
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.snapshot-hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.snapshot-close {
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.snapshot-toolbar {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.snapshot-checkbox {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.snapshot-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.snapshot-pre {
|
||||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
overflow: auto;
|
||||
background: #0b1020;
|
||||
color: #dbeafe;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preset-section {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ const ConfigPanel = () => {
|
|||
const [systemStatus, setSystemStatus] = useState(null)
|
||||
const [systemBusy, setSystemBusy] = useState(false)
|
||||
|
||||
// 配置快照(用于整体分析/导出)
|
||||
const [showSnapshot, setShowSnapshot] = useState(false)
|
||||
const [snapshotText, setSnapshotText] = useState('')
|
||||
const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false)
|
||||
const [snapshotBusy, setSnapshotBusy] = useState(false)
|
||||
|
||||
// 预设方案配置
|
||||
// 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(0.08)
|
||||
const presets = {
|
||||
|
|
@ -199,6 +205,134 @@ const ConfigPanel = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const isSecretKey = (key) => {
|
||||
return key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET'
|
||||
}
|
||||
|
||||
const maskSecret = (val) => {
|
||||
const s = val === null || val === undefined ? '' : String(val)
|
||||
if (!s) return ''
|
||||
if (s.length <= 8) return '****'
|
||||
return `${s.slice(0, 4)}...${s.slice(-4)}`
|
||||
}
|
||||
|
||||
const toDisplayValueForSnapshot = (key, value) => {
|
||||
if (value === null || value === undefined) return value
|
||||
// 百分比/比例:尽量转成 “百分比值”
|
||||
if (typeof value === 'number' && (key.includes('PERCENT') || key.includes('PCT'))) {
|
||||
// 存储一般是 0~1,小于 1 则乘 100;若已经是 >=1 则认为就是百分比
|
||||
return value < 1 ? value * 100 : value
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const buildConfigSnapshot = async (includeSecrets) => {
|
||||
// 为了确保是最新值,这里点击时再拉一次
|
||||
const data = await api.getConfigs()
|
||||
const now = new Date()
|
||||
|
||||
const categoryMap = {
|
||||
scan: '市场扫描',
|
||||
position: '仓位控制',
|
||||
risk: '风险控制',
|
||||
strategy: '策略参数',
|
||||
api: 'API配置',
|
||||
}
|
||||
|
||||
const entries = Object.entries(data || {}).map(([key, cfg]) => {
|
||||
const rawVal = cfg?.value
|
||||
const valMasked = isSecretKey(key) && !includeSecrets ? maskSecret(rawVal) : rawVal
|
||||
const displayVal = toDisplayValueForSnapshot(key, valMasked)
|
||||
return {
|
||||
key,
|
||||
category: cfg?.category || '',
|
||||
category_label: categoryMap[cfg?.category] || cfg?.category || '',
|
||||
type: cfg?.type || '',
|
||||
value: valMasked,
|
||||
display_value: displayVal,
|
||||
description: cfg?.description || '',
|
||||
}
|
||||
})
|
||||
|
||||
// 排序:先分类再按 key
|
||||
entries.sort((a, b) => {
|
||||
const ca = a.category_label || a.category || ''
|
||||
const cb = b.category_label || b.category || ''
|
||||
if (ca !== cb) return ca.localeCompare(cb)
|
||||
return a.key.localeCompare(b.key)
|
||||
})
|
||||
|
||||
const snapshot = {
|
||||
fetched_at: now.toISOString(),
|
||||
note: 'display_value 对 PERCENT/PCT 做了百分比换算;敏感字段可选择脱敏/明文。',
|
||||
preset_detected: detectCurrentPreset(),
|
||||
system_status: systemStatus
|
||||
? {
|
||||
running: !!systemStatus.running,
|
||||
pid: systemStatus.pid || null,
|
||||
program: systemStatus.program || null,
|
||||
state: systemStatus.state || null,
|
||||
}
|
||||
: null,
|
||||
feasibility_check_summary: feasibilityCheck
|
||||
? {
|
||||
feasible: !!feasibilityCheck.feasible,
|
||||
account_balance: feasibilityCheck.account_balance ?? null,
|
||||
base_leverage: feasibilityCheck.base_leverage ?? feasibilityCheck.leverage ?? null,
|
||||
max_leverage: feasibilityCheck.max_leverage ?? null,
|
||||
use_dynamic_leverage: feasibilityCheck.use_dynamic_leverage ?? null,
|
||||
min_margin_usdt: feasibilityCheck.current_config?.min_margin_usdt ?? null,
|
||||
}
|
||||
: null,
|
||||
configs: entries,
|
||||
}
|
||||
|
||||
return JSON.stringify(snapshot, null, 2)
|
||||
}
|
||||
|
||||
const openSnapshot = async (includeSecrets) => {
|
||||
setSnapshotBusy(true)
|
||||
setMessage('')
|
||||
try {
|
||||
const text = await buildConfigSnapshot(includeSecrets)
|
||||
setSnapshotText(text)
|
||||
setShowSnapshot(true)
|
||||
} catch (e) {
|
||||
setMessage('生成配置快照失败: ' + (e?.message || '未知错误'))
|
||||
} finally {
|
||||
setSnapshotBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const copySnapshot = async () => {
|
||||
try {
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(snapshotText || '')
|
||||
setMessage('已复制配置快照到剪贴板')
|
||||
} else {
|
||||
setMessage('当前浏览器不支持剪贴板 API,可手动全选复制')
|
||||
}
|
||||
} catch (e) {
|
||||
setMessage('复制失败: ' + (e?.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const downloadSnapshot = () => {
|
||||
try {
|
||||
const blob = new Blob([snapshotText || ''], { type: 'application/json;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `config-snapshot-${new Date().toISOString().replace(/[:.]/g, '-')}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (e) {
|
||||
setMessage('下载失败: ' + (e?.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
// 检测当前配置匹配哪个预设方案
|
||||
const detectCurrentPreset = () => {
|
||||
if (!configs || Object.keys(configs).length === 0) return null
|
||||
|
|
@ -316,8 +450,19 @@ const ConfigPanel = () => {
|
|||
<div className="config-header">
|
||||
<div className="header-top">
|
||||
<h2>交易配置</h2>
|
||||
<div className="header-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="guide-link snapshot-btn"
|
||||
onClick={() => openSnapshot(snapshotIncludeSecrets)}
|
||||
disabled={snapshotBusy}
|
||||
title="导出当前全量配置(用于分析)"
|
||||
>
|
||||
{snapshotBusy ? '生成中...' : '查看整体配置'}
|
||||
</button>
|
||||
<Link to="/config/guide" className="guide-link">📖 配置说明</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="config-info">
|
||||
<p>修改配置后,交易系统将在下次扫描时自动使用新配置</p>
|
||||
</div>
|
||||
|
|
@ -552,6 +697,51 @@ const ConfigPanel = () => {
|
|||
</div>
|
||||
</section>
|
||||
))}
|
||||
|
||||
{showSnapshot && (
|
||||
<div className="snapshot-modal-overlay" onClick={() => setShowSnapshot(false)} role="presentation">
|
||||
<div className="snapshot-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="snapshot-modal-header">
|
||||
<div>
|
||||
<h3>当前整体配置快照</h3>
|
||||
<div className="snapshot-hint">
|
||||
默认脱敏 BINANCE_API_KEY/SECRET。你可以选择明文后重新生成再复制/下载。
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="snapshot-close" onClick={() => setShowSnapshot(false)}>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="snapshot-toolbar">
|
||||
<label className="snapshot-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={snapshotIncludeSecrets}
|
||||
onChange={async (e) => {
|
||||
const checked = e.target.checked
|
||||
setSnapshotIncludeSecrets(checked)
|
||||
// 重新生成(确保脱敏/明文一致)
|
||||
await openSnapshot(checked)
|
||||
}}
|
||||
/>
|
||||
显示敏感信息(明文)
|
||||
</label>
|
||||
|
||||
<div className="snapshot-actions">
|
||||
<button type="button" className="system-btn" onClick={copySnapshot}>
|
||||
复制
|
||||
</button>
|
||||
<button type="button" className="system-btn primary" onClick={downloadSnapshot}>
|
||||
下载 JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre className="snapshot-pre">{snapshotText}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user