This commit is contained in:
薇薇安 2026-01-18 20:59:55 +08:00
parent 8bfe9d95da
commit 31486b5026
2 changed files with 293 additions and 1 deletions

View File

@ -41,11 +41,113 @@
transition: all 0.3s; 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 { .guide-link:hover {
background: #2196F3; background: #2196F3;
color: white; 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 { .preset-section {
background: #f8f9fa; background: #f8f9fa;
padding: 1rem; padding: 1rem;

View File

@ -13,6 +13,12 @@ const ConfigPanel = () => {
const [systemStatus, setSystemStatus] = useState(null) const [systemStatus, setSystemStatus] = useState(null)
const [systemBusy, setSystemBusy] = useState(false) 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.08%0.08 // 使8.08%0.08
const presets = { 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 = () => { const detectCurrentPreset = () => {
if (!configs || Object.keys(configs).length === 0) return null if (!configs || Object.keys(configs).length === 0) return null
@ -316,7 +450,18 @@ const ConfigPanel = () => {
<div className="config-header"> <div className="config-header">
<div className="header-top"> <div className="header-top">
<h2>交易配置</h2> <h2>交易配置</h2>
<Link to="/config/guide" className="guide-link">📖 配置说明</Link> <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>
<div className="config-info"> <div className="config-info">
<p>修改配置后交易系统将在下次扫描时自动使用新配置</p> <p>修改配置后交易系统将在下次扫描时自动使用新配置</p>
@ -552,6 +697,51 @@ const ConfigPanel = () => {
</div> </div>
</section> </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> </div>
) )
} }