auto_trade_sys/frontend/src/components/GlobalConfig.jsx
薇薇安 3ef66fb54a a
2026-01-22 11:41:32 +08:00

1157 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from 'react'
import { Link } from 'react-router-dom'
import { api, getCurrentAccountId } from '../services/api'
import './GlobalConfig.css'
import './ConfigPanel.css' // 复用 ConfigPanel 的样式
const GlobalConfig = ({ currentUser }) => {
const [users, setUsers] = useState([])
const [accounts, setAccounts] = useState([])
const [loading, setLoading] = useState(true)
const [message, setMessage] = useState('')
const [busy, setBusy] = useState(false)
const [selectedUserId, setSelectedUserId] = useState(null)
const [showUserForm, setShowUserForm] = useState(false)
const [newUser, setNewUser] = useState({ username: '', password: '', role: 'user', status: 'active' })
const [editingUserId, setEditingUserId] = useState(null)
// 系统控制相关
const [systemStatus, setSystemStatus] = useState(null)
const [backendStatus, setBackendStatus] = useState(null)
const [systemBusy, setSystemBusy] = useState(false)
// 预设方案相关
const [configs, setConfigs] = useState({})
const [saving, setSaving] = useState(false)
const [currentAccountId, setCurrentAccountId] = useState(getCurrentAccountId())
const [configMeta, setConfigMeta] = useState(null)
// 配置快照相关
const [showSnapshot, setShowSnapshot] = useState(false)
const [snapshotText, setSnapshotText] = useState('')
const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false)
const [snapshotBusy, setSnapshotBusy] = useState(false)
const PCT_LIKE_KEYS = new Set([
'LIMIT_ORDER_OFFSET_PCT',
'ENTRY_MAX_DRIFT_PCT_TRENDING',
'ENTRY_MAX_DRIFT_PCT_RANGING',
])
// 预设方案配置(必须在函数定义之前,常量定义)
const presets = {
swing: {
name: '波段回归(推荐)',
desc: '根治高频与追价:关闭智能入场,回归"纯限价 + 30分钟扫描 + 更高信号门槛"的低频波段。建议先跑20-30单再评估。',
configs: {
SCAN_INTERVAL: 1800,
TOP_N_SYMBOLS: 8,
MAX_POSITION_PERCENT: 2.0,
MAX_TOTAL_POSITION_PERCENT: 20.0,
MIN_POSITION_PERCENT: 0.0,
MIN_SIGNAL_STRENGTH: 8,
USE_TRAILING_STOP: false,
ATR_TAKE_PROFIT_MULTIPLIER: 4.5,
TAKE_PROFIT_PERCENT: 25.0,
MIN_HOLD_TIME_SEC: 1800,
SMART_ENTRY_ENABLED: false,
},
},
fill: {
name: '成交优先(更少漏单)',
desc: '优先解决"挂单NEW→超时撤单→没成交"的问题:解锁自动交易过滤 + 保守智能入场(限制追价步数与追价上限),在趋势强时允许可控的市价兜底。',
configs: {
SCAN_INTERVAL: 1800,
TOP_N_SYMBOLS: 6,
MIN_SIGNAL_STRENGTH: 7,
AUTO_TRADE_ONLY_TRENDING: false,
AUTO_TRADE_ALLOW_4H_NEUTRAL: true,
SMART_ENTRY_ENABLED: true,
LIMIT_ORDER_OFFSET_PCT: 0.1,
ENTRY_CONFIRM_TIMEOUT_SEC: 120,
ENTRY_CHASE_MAX_STEPS: 2,
ENTRY_STEP_WAIT_SEC: 20,
ENTRY_MARKET_FALLBACK_AFTER_SEC: 60,
ENTRY_MAX_DRIFT_PCT_TRENDING: 0.3,
ENTRY_MAX_DRIFT_PCT_RANGING: 0.15,
USE_TRAILING_STOP: false,
ATR_TAKE_PROFIT_MULTIPLIER: 4.5,
TAKE_PROFIT_PERCENT: 25.0,
MIN_HOLD_TIME_SEC: 1800,
},
},
strict: {
name: '精选低频(高胜率倾向)',
desc: '更偏"少单、质量优先":仅趋势行情自动交易 + 4H中性不自动下单 + 更高信号门槛。仍保持较贴近的限价偏移,减少"完全成交不了"。',
configs: {
SCAN_INTERVAL: 1800,
TOP_N_SYMBOLS: 6,
MIN_SIGNAL_STRENGTH: 8,
AUTO_TRADE_ONLY_TRENDING: true,
AUTO_TRADE_ALLOW_4H_NEUTRAL: false,
SMART_ENTRY_ENABLED: false,
LIMIT_ORDER_OFFSET_PCT: 0.1,
ENTRY_CONFIRM_TIMEOUT_SEC: 180,
USE_TRAILING_STOP: false,
ATR_TAKE_PROFIT_MULTIPLIER: 4.5,
TAKE_PROFIT_PERCENT: 25.0,
MIN_HOLD_TIME_SEC: 1800,
},
},
steady: {
name: '稳定出单(均衡收益/频率)',
desc: '在"会下单"的基础上略提高出单频率:更短扫描间隔 + 更宽松门槛 + 保守智能入场(追价受限),适合想要稳定有单但不想回到高频。',
configs: {
SCAN_INTERVAL: 900,
TOP_N_SYMBOLS: 8,
MIN_SIGNAL_STRENGTH: 6,
AUTO_TRADE_ONLY_TRENDING: false,
AUTO_TRADE_ALLOW_4H_NEUTRAL: true,
SMART_ENTRY_ENABLED: true,
LIMIT_ORDER_OFFSET_PCT: 0.12,
ENTRY_CONFIRM_TIMEOUT_SEC: 120,
ENTRY_CHASE_MAX_STEPS: 3,
ENTRY_STEP_WAIT_SEC: 15,
ENTRY_MARKET_FALLBACK_AFTER_SEC: 45,
ENTRY_MAX_DRIFT_PCT_TRENDING: 0.4,
ENTRY_MAX_DRIFT_PCT_RANGING: 0.2,
USE_TRAILING_STOP: false,
ATR_TAKE_PROFIT_MULTIPLIER: 4.5,
TAKE_PROFIT_PERCENT: 25.0,
MIN_HOLD_TIME_SEC: 1800,
},
},
conservative: {
name: '保守配置',
desc: '适合新手,风险较低,止损止盈较宽松,避免被正常波动触发',
configs: {
SCAN_INTERVAL: 900,
MIN_CHANGE_PERCENT: 2.0,
MIN_SIGNAL_STRENGTH: 5,
TOP_N_SYMBOLS: 10,
MAX_SCAN_SYMBOLS: 150,
MIN_VOLATILITY: 0.02,
STOP_LOSS_PERCENT: 10.0,
TAKE_PROFIT_PERCENT: 25.0,
MIN_STOP_LOSS_PRICE_PCT: 2.0,
MIN_TAKE_PROFIT_PRICE_PCT: 3.0,
USE_TRAILING_STOP: false,
ATR_TAKE_PROFIT_MULTIPLIER: 4.5,
MIN_HOLD_TIME_SEC: 1800
}
},
balanced: {
name: '平衡配置',
desc: '推荐使用平衡频率和质量止损止盈适中盈亏比2.5:1',
configs: {
SCAN_INTERVAL: 600,
MIN_CHANGE_PERCENT: 1.5,
MIN_SIGNAL_STRENGTH: 4,
TOP_N_SYMBOLS: 12,
MAX_SCAN_SYMBOLS: 250,
MIN_VOLATILITY: 0.018,
STOP_LOSS_PERCENT: 8.0,
TAKE_PROFIT_PERCENT: 25.0,
MIN_STOP_LOSS_PRICE_PCT: 2.0,
MIN_TAKE_PROFIT_PRICE_PCT: 3.0,
USE_TRAILING_STOP: false,
ATR_TAKE_PROFIT_MULTIPLIER: 4.5,
MIN_HOLD_TIME_SEC: 1800
}
},
aggressive: {
name: '激进高频',
desc: '晚间波动大时使用交易频率高止损较紧但止盈合理盈亏比3:1',
configs: {
SCAN_INTERVAL: 300,
MIN_CHANGE_PERCENT: 1.0,
MIN_SIGNAL_STRENGTH: 3,
TOP_N_SYMBOLS: 18,
MAX_SCAN_SYMBOLS: 350,
MIN_VOLATILITY: 0.015,
STOP_LOSS_PERCENT: 5.0,
TAKE_PROFIT_PERCENT: 25.0,
MIN_STOP_LOSS_PRICE_PCT: 1.5,
MIN_TAKE_PROFIT_PRICE_PCT: 2.0,
USE_TRAILING_STOP: false,
ATR_TAKE_PROFIT_MULTIPLIER: 4.5,
MIN_HOLD_TIME_SEC: 1800
}
}
}
// 所有函数定义(必须在 useEffect 之前)
const loadUsers = async () => {
try {
const list = await api.getUsers()
setUsers(Array.isArray(list) ? list : [])
} catch (error) {
setMessage('加载用户列表失败: ' + (error.message || '未知错误'))
} finally {
setLoading(false)
}
}
const loadAccounts = async () => {
try {
const list = await api.getAccounts()
setAccounts(Array.isArray(list) ? list : [])
} catch (error) {
console.error('加载账号列表失败:', error)
}
}
const loadConfigMeta = async () => {
try {
const m = await api.getConfigMeta()
setConfigMeta(m || null)
} catch (e) {
setConfigMeta(null)
}
}
const loadConfigs = async () => {
try {
const data = await api.getConfigs()
setConfigs(data)
} catch (error) {
console.error('Failed to load configs:', error)
}
}
const loadSystemStatus = async () => {
try {
const res = await api.getTradingSystemStatus()
setSystemStatus(res)
} catch (error) {
// 静默失败
}
}
const loadBackendStatus = async () => {
try {
const res = await api.getBackendStatus()
setBackendStatus(res)
} catch (error) {
// 静默失败
}
}
// 检测当前配置匹配哪个预设方案
const detectCurrentPreset = () => {
if (!configs || Object.keys(configs).length === 0) return null
for (const [presetKey, preset] of Object.entries(presets)) {
let match = true
for (const [key, expectedValue] of Object.entries(preset.configs)) {
const currentConfig = configs[key]
if (!currentConfig) {
match = false
break
}
let currentValue = currentConfig.value
if (key.includes('PERCENT') || key.includes('PCT')) {
if (PCT_LIKE_KEYS.has(key)) {
currentValue = currentValue <= 0.05 ? currentValue * 100 : currentValue
} else {
currentValue = currentValue * 100
}
}
if (typeof expectedValue === 'number' && typeof currentValue === 'number') {
if (Math.abs(currentValue - expectedValue) > 0.01) {
match = false
break
}
} else if (currentValue !== expectedValue) {
match = false
break
}
}
if (match) {
return presetKey
}
}
return null
}
useEffect(() => {
loadUsers()
loadAccounts()
if (isAdmin) {
loadConfigMeta()
loadConfigs()
loadSystemStatus()
loadBackendStatus()
const timer = setInterval(() => {
loadSystemStatus()
loadBackendStatus()
}, 3000)
return () => clearInterval(timer)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAdmin])
// 系统控制函数
const handleClearCache = async () => {
setSystemBusy(true)
setMessage('')
try {
const res = await api.clearSystemCache()
setMessage(res.message || '缓存已清理')
await loadConfigs()
await loadSystemStatus()
} catch (error) {
setMessage('清理缓存失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const handleStartTrading = async () => {
setSystemBusy(true)
setMessage('')
try {
const res = await api.startTradingSystem()
setMessage(res.message || '交易系统已启动')
await loadSystemStatus()
} catch (error) {
setMessage('启动失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const handleStopTrading = async () => {
setSystemBusy(true)
setMessage('')
try {
const res = await api.stopTradingSystem()
setMessage(res.message || '交易系统已停止')
await loadSystemStatus()
} catch (error) {
setMessage('停止失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const handleRestartTrading = async () => {
setSystemBusy(true)
setMessage('')
try {
const res = await api.restartTradingSystem()
setMessage(res.message || '交易系统已重启')
await loadSystemStatus()
} catch (error) {
setMessage('重启失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const handleRestartBackend = async () => {
if (!window.confirm('确定要重启后端服务吗?重启期间页面接口会短暂不可用(约 3-10 秒)。')) return
setSystemBusy(true)
setMessage('')
try {
const res = await api.restartBackend()
setMessage(res.message || '已发起后端重启')
setTimeout(() => {
loadBackendStatus()
}, 4000)
} catch (error) {
setMessage('重启后端失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const handleRestartAllTrading = async () => {
if (!window.confirm('确定要重启【所有账号】的交易进程吗?这会让所有用户的交易服务短暂中断(约 3-10 秒),用于升级代码后统一生效。')) return
setSystemBusy(true)
setMessage('')
try {
const res = await api.restartAllTradingSystems({ prefix: 'auto_sys_acc', do_update: true })
setMessage(`已发起批量重启:共 ${res.count} 个,成功 ${res.ok},失败 ${res.failed}`)
} catch (e) {
setMessage('批量重启失败: ' + (e?.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const applyPreset = async (presetKey) => {
const preset = presets[presetKey]
if (!preset) return
setSaving(true)
setMessage('')
try {
const configItems = Object.entries(preset.configs).map(([key, value]) => {
const config = configs[key]
if (!config) {
let type = 'number'
let category = 'risk'
if (typeof value === 'boolean') {
type = 'boolean'
category = 'strategy'
}
if (key.startsWith('ENTRY_') || key.startsWith('SMART_ENTRY_') || key === 'SMART_ENTRY_ENABLED') {
type = typeof value === 'boolean' ? 'boolean' : 'number'
category = 'strategy'
} else if (key.startsWith('AUTO_TRADE_')) {
type = typeof value === 'boolean' ? 'boolean' : 'number'
category = 'strategy'
} else if (key === 'LIMIT_ORDER_OFFSET_PCT') {
type = 'number'
category = 'strategy'
} else if (key.includes('PERCENT') || key.includes('PCT')) {
type = 'number'
if (key.includes('STOP_LOSS') || key.includes('TAKE_PROFIT')) {
category = 'risk'
} else if (key.includes('POSITION')) {
category = 'position'
} else {
category = 'scan'
}
} else if (key === 'MIN_VOLATILITY') {
type = 'number'
category = 'scan'
} else if (typeof value === 'number') {
type = 'number'
category = 'scan'
}
return {
key,
value: (key.includes('PERCENT') || key.includes('PCT')) && !PCT_LIKE_KEYS.has(key) ? value / 100 : value,
type,
category,
description: `预设方案配置项:${key}`
}
}
return {
key,
value: (key.includes('PERCENT') || key.includes('PCT')) && !PCT_LIKE_KEYS.has(key) ? value / 100 : value,
type: config.type,
category: config.category,
description: config.description
}
}).filter(Boolean)
const response = await api.updateConfigsBatch(configItems)
setMessage(response.message || `已应用${preset.name}`)
if (response.note) {
setTimeout(() => {
setMessage(response.note)
}, 2000)
}
await loadConfigs()
} catch (error) {
setMessage('应用预设失败: ' + error.message)
} finally {
setSaving(false)
}
}
// 配置快照函数
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'))) {
if (PCT_LIKE_KEYS.has(key)) {
return value <= 0.05 ? value * 100 : value
}
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 || '',
}
})
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 tempConfigs = data || {}
let detectedPreset = null
for (const [presetKey, preset] of Object.entries(presets)) {
let match = true
for (const [key, expectedValue] of Object.entries(preset.configs)) {
const currentConfig = tempConfigs[key]
if (!currentConfig) {
match = false
break
}
let currentValue = currentConfig.value
if (key.includes('PERCENT') || key.includes('PCT')) {
if (PCT_LIKE_KEYS.has(key)) {
currentValue = currentValue <= 0.05 ? currentValue * 100 : currentValue
} else {
currentValue = currentValue * 100
}
}
if (typeof expectedValue === 'number' && typeof currentValue === 'number') {
if (Math.abs(currentValue - expectedValue) > 0.01) {
match = false
break
}
} else if (currentValue !== expectedValue) {
match = false
break
}
}
if (match) {
detectedPreset = presetKey
break
}
}
const snapshot = {
fetched_at: now.toISOString(),
note: 'display_value 对 PERCENT/PCT 做了百分比换算;敏感字段可选择脱敏/明文。',
preset_detected: detectedPreset,
system_status: systemStatus ? {
running: !!systemStatus.running,
pid: systemStatus.pid || null,
program: systemStatus.program || null,
state: systemStatus.state || 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 handleCreateUser = async () => {
if (!newUser.username || !newUser.password) {
setMessage('用户名和密码不能为空')
return
}
setBusy(true)
setMessage('')
try {
await api.createUser(newUser)
setMessage('用户创建成功')
setShowUserForm(false)
setNewUser({ username: '', password: '', role: 'user', status: 'active' })
await loadUsers()
} catch (error) {
setMessage('创建用户失败: ' + (error.message || '未知错误'))
} finally {
setBusy(false)
}
}
const handleUpdateUserPassword = async (userId) => {
const passwordInput = document.querySelector(`input[data-user-id="${userId}"]`)
const password = passwordInput?.value
if (!password) {
setMessage('密码不能为空')
return
}
setBusy(true)
setMessage('')
try {
await api.updateUserPassword(userId, password)
setMessage('密码更新成功')
setEditingUserId(null)
if (passwordInput) passwordInput.value = ''
await loadUsers()
} catch (error) {
setMessage('更新密码失败: ' + (error.message || '未知错误'))
} finally {
setBusy(false)
}
}
const handleUpdateUserRole = async (userId, role) => {
setBusy(true)
setMessage('')
try {
await api.updateUserRole(userId, role)
setMessage('角色更新成功')
await loadUsers()
} catch (error) {
setMessage('更新角色失败: ' + (error.message || '未知错误'))
} finally {
setBusy(false)
}
}
const handleUpdateUserStatus = async (userId, status) => {
setBusy(true)
setMessage('')
try {
await api.updateUserStatus(userId, status)
setMessage('状态更新成功')
await loadUsers()
} catch (error) {
setMessage('更新状态失败: ' + (error.message || '未知错误'))
} finally {
setBusy(false)
}
}
if (loading) {
return <div className="global-config">加载中...</div>
}
// 简单计算全局策略账号ID
const globalStrategyAccountId = configMeta?.global_strategy_account_id ? parseInt(String(configMeta.global_strategy_account_id), 10) : 1
const isGlobalStrategyAccount = isAdmin && currentAccountId === globalStrategyAccountId
// 简单计算:当前预设(直接在 render 时计算,不使用 useMemo
let currentPreset = null
if (configs && Object.keys(configs).length > 0) {
try {
currentPreset = detectCurrentPreset()
} catch (e) {
console.error('detectCurrentPreset error:', e)
}
}
const presetUiMeta = {
swing: { group: 'limit', tag: '纯限价' },
strict: { group: 'limit', tag: '纯限价' },
fill: { group: 'smart', tag: '智能入场' },
steady: { group: 'smart', tag: '智能入场' },
conservative: { group: 'legacy', tag: '传统' },
balanced: { group: 'legacy', tag: '传统' },
aggressive: { group: 'legacy', tag: '高频实验' },
}
const presetGroups = [
{
key: 'limit',
title: 'A. 纯限价SMART_ENTRY_ENABLED=false',
desc: '只下 1 次限价单,未在确认时间内成交就撤单跳过。更控频、更接近"波段",但更容易出现 NEW→撤单。',
presetKeys: ['swing', 'strict'],
},
{
key: 'smart',
title: 'B. 智能入场SMART_ENTRY_ENABLED=true',
desc: '限价回调 + 受限追价 +(趋势强时)可控市价兜底。更少漏单,但必须限制追价步数与偏离上限,避免回到高频追价。',
presetKeys: ['fill', 'steady'],
},
{
key: 'legacy',
title: 'C. 传统 / 实验(不建议长期)',
desc: '这组更多用于对比或临时实验(频率更高/更容易过度交易),建议在稳定盈利前谨慎使用。',
presetKeys: ['conservative', 'balanced', 'aggressive'],
},
]
return (
<div className="global-config">
<div className="global-config-header">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h2>全局配置</h2>
<p>管理用户账号和全局策略配置</p>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<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>
{message && (
<div className={`message ${message.includes('失败') ? 'error' : 'success'}`}>
{message}
</div>
)}
{/* 系统控制 */}
{isAdmin && (
<section className="global-section system-section">
<div className="system-header">
<h3>系统控制</h3>
<div className="system-status">
<span className={`system-status-badge ${systemStatus?.running ? 'running' : 'stopped'}`}>
{systemStatus?.running ? '运行中' : '未运行'}
</span>
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null}
{systemStatus?.program ? <span className="system-status-meta">程序: {systemStatus.program}</span> : null}
{systemStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {systemStatus.meta.requested_at}</span> : null}
</div>
</div>
<div className="system-actions">
<button
type="button"
className="system-btn"
onClick={handleClearCache}
disabled={systemBusy}
title="清理Redis配置缓存并从数据库回灌。切换API Key后建议先点这里再重启交易系统。"
>
清除缓存
</button>
<button
type="button"
className="system-btn"
onClick={handleStopTrading}
disabled={systemBusy || systemStatus?.running === false}
title="通过 supervisorctl 停止交易系统"
>
停止
</button>
<button
type="button"
className="system-btn"
onClick={handleStartTrading}
disabled={systemBusy || systemStatus?.running === true}
title="通过 supervisorctl 启动交易系统"
>
启动
</button>
<button
type="button"
className="system-btn primary"
onClick={handleRestartTrading}
disabled={systemBusy}
title="通过 supervisorctl 重启交易系统建议切换API Key后使用"
>
重启交易系统
</button>
<button
type="button"
className="system-btn primary"
onClick={handleRestartAllTrading}
disabled={systemBusy}
title="批量重启所有账号交易进程auto_sys_acc*),用于代码升级后统一生效"
>
重启所有账号交易
</button>
<button
type="button"
className="system-btn primary"
onClick={handleRestartBackend}
disabled={systemBusy}
title="通过 backend/restart.sh 重启后端uvicorn。重启期间接口会短暂不可用。"
>
重启后端服务
</button>
</div>
<div className="system-status" style={{ marginTop: '10px' }}>
<span className={`system-status-badge ${backendStatus?.running ? 'running' : 'stopped'}`}>
后端 {backendStatus?.running ? '运行中' : '未知'}
</span>
{backendStatus?.pid ? <span className="system-status-meta">PID: {backendStatus.pid}</span> : null}
{backendStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {backendStatus.meta.requested_at}</span> : null}
</div>
<div className="system-hint">
建议流程先更新配置里的 Key 点击"清除缓存" 点击"重启交易系统"确保不再使用旧账号下单
</div>
</section>
)}
{/* 预设方案快速切换(仅管理员 + 全局策略账号) */}
{isAdmin && isGlobalStrategyAccount && (
<section className="global-section preset-section">
<div className="preset-header">
<h3>快速切换方案</h3>
<div className="current-preset-status">
<span className="status-label">当前方案</span>
<span className={`status-badge ${currentPreset ? 'preset' : 'custom'}`}>
{currentPreset ? presets[currentPreset].name : '自定义'}
</span>
</div>
</div>
<div className="preset-guide">
<div className="preset-guide-title">怎么选更不迷糊</div>
<ul className="preset-guide-list">
<li>
<strong>先选入场机制</strong> vs
</li>
<li>
<strong>再看"会不会下单"</strong> <code>AUTO_TRADE_ONLY_TRENDING</code> <code>AUTO_TRADE_ALLOW_4H_NEUTRAL</code>
</li>
<li>
<strong>最后再微调</strong> <code>LIMIT_ORDER_OFFSET_PCT</code> <code>ENTRY_CONFIRM_TIMEOUT_SEC</code>
</li>
</ul>
</div>
<div className="preset-groups">
{presetGroups.map((g) => (
<div key={g.key} className="preset-group">
<div className="preset-group-header">
<div className="preset-group-title">{g.title}</div>
<div className="preset-group-desc">{g.desc}</div>
</div>
<div className="preset-buttons">
{g.presetKeys
.filter((k) => presets[k])
.map((k) => {
const preset = presets[k]
const meta = presetUiMeta[k] || { group: g.key, tag: '' }
return (
<button
key={k}
className={`preset-btn ${currentPreset === k ? 'active' : ''}`}
onClick={() => applyPreset(k)}
disabled={saving}
title={preset.desc}
>
<div className="preset-name">
{preset.name}
{meta.tag ? (
<span className={`preset-tag preset-tag--${meta.group}`}>{meta.tag}</span>
) : null}
{currentPreset === k ? <span className="active-indicator"></span> : null}
</div>
<div className="preset-desc">{preset.desc}</div>
</button>
)
})}
</div>
</div>
))}
</div>
</section>
)}
{/* 用户管理 */}
<section className="global-section">
<div className="section-header">
<h3>用户管理</h3>
<button
type="button"
className="btn-primary"
onClick={() => setShowUserForm(!showUserForm)}
disabled={busy}
>
{showUserForm ? '取消' : '+ 创建用户'}
</button>
</div>
{showUserForm && (
<div className="form-card">
<h4>创建新用户</h4>
<div className="form-group">
<label>用户名</label>
<input
type="text"
value={newUser.username}
onChange={(e) => setNewUser({ ...newUser, username: e.target.value })}
placeholder="输入用户名"
/>
</div>
<div className="form-group">
<label>密码</label>
<input
type="password"
value={newUser.password}
onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
placeholder="输入密码"
/>
</div>
<div className="form-group">
<label>角色</label>
<select
value={newUser.role}
onChange={(e) => setNewUser({ ...newUser, role: e.target.value })}
>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
<div className="form-group">
<label>状态</label>
<select
value={newUser.status}
onChange={(e) => setNewUser({ ...newUser, status: e.target.value })}
>
<option value="active">启用</option>
<option value="disabled">禁用</option>
</select>
</div>
<div className="form-actions">
<button type="button" className="btn-primary" onClick={handleCreateUser} disabled={busy}>
创建
</button>
<button type="button" onClick={() => setShowUserForm(false)} disabled={busy}>
取消
</button>
</div>
</div>
)}
<div className="table-container">
<table className="data-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>角色</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.username}</td>
<td>
<select
value={user.role || 'user'}
onChange={(e) => handleUpdateUserRole(user.id, e.target.value)}
disabled={busy}
>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</td>
<td>
<select
value={user.status || 'active'}
onChange={(e) => handleUpdateUserStatus(user.id, e.target.value)}
disabled={busy}
>
<option value="active">启用</option>
<option value="disabled">禁用</option>
</select>
</td>
<td>
{editingUserId === user.id ? (
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<input
type="password"
data-user-id={user.id}
placeholder="新密码"
style={{ padding: '4px 8px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '14px' }}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleUpdateUserPassword(user.id)
}
}}
/>
<button
type="button"
className="btn-primary"
onClick={() => handleUpdateUserPassword(user.id)}
disabled={busy}
>
保存
</button>
<button
type="button"
onClick={() => {
setEditingUserId(null)
const input = document.querySelector(`input[data-user-id="${user.id}"]`)
if (input) input.value = ''
}}
disabled={busy}
>
取消
</button>
</div>
) : (
<button
type="button"
onClick={() => setEditingUserId(user.id)}
disabled={busy}
>
修改密码
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* 账号管理 */}
<section className="global-section">
<div className="section-header">
<h3>账号管理</h3>
<button
type="button"
className="btn-primary"
onClick={loadAccounts}
disabled={busy}
>
刷新
</button>
</div>
<div className="table-container">
<table className="data-table">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>状态</th>
<th>测试网</th>
<th>API Key</th>
</tr>
</thead>
<tbody>
{accounts.map((account) => (
<tr key={account.id}>
<td>{account.id}</td>
<td>{account.name || '未命名'}</td>
<td>
<span className={`status-badge ${account.status === 'active' ? 'active' : 'disabled'}`}>
{account.status === 'active' ? '启用' : '禁用'}
</span>
</td>
<td>{account.use_testnet ? '是' : '否'}</td>
<td>{account.has_api_key ? '已配置' : '未配置'}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* 配置快照 Modal */}
{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>
)
}
export default GlobalConfig