diff --git a/frontend/src/components/ConfigPanel.css b/frontend/src/components/ConfigPanel.css index b5b086e..fc282e7 100644 --- a/frontend/src/components/ConfigPanel.css +++ b/frontend/src/components/ConfigPanel.css @@ -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; diff --git a/frontend/src/components/ConfigPanel.jsx b/frontend/src/components/ConfigPanel.jsx index e64852a..245cc52 100644 --- a/frontend/src/components/ConfigPanel.jsx +++ b/frontend/src/components/ConfigPanel.jsx @@ -12,6 +12,12 @@ const ConfigPanel = () => { const [checkingFeasibility, setCheckingFeasibility] = useState(false) 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) @@ -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,7 +450,18 @@ const ConfigPanel = () => {

交易配置

- 📖 配置说明 +
+ + 📖 配置说明 +

修改配置后,交易系统将在下次扫描时自动使用新配置

@@ -552,6 +697,51 @@ const ConfigPanel = () => {
))} + + {showSnapshot && ( +
setShowSnapshot(false)} role="presentation"> +
e.stopPropagation()}> +
+
+

当前整体配置快照

+
+ 默认脱敏 BINANCE_API_KEY/SECRET。你可以选择明文后重新生成再复制/下载。 +
+
+ +
+ +
+ + +
+ + +
+
+ +
{snapshotText}
+
+
+ )}
) }