a
This commit is contained in:
parent
8bfe9d95da
commit
31486b5026
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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.0表示8%),在应用时会转换为小数(0.08)
|
// 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user