diff --git a/backend/api/routes/system.py b/backend/api/routes/system.py index 3bcbb7c..48ff2e5 100644 --- a/backend/api/routes/system.py +++ b/backend/api/routes/system.py @@ -895,6 +895,79 @@ async def trading_restart(_admin: Dict[str, Any] = Depends(require_system_admin) raise HTTPException(status_code=500, detail=f"supervisorctl restart 失败: {e}") +@router.post("/trading/stop-all") +async def trading_stop_all( + _admin: Dict[str, Any] = Depends(require_system_admin), + prefix: str = "auto_sys_acc", + include_default: bool = False, +) -> Dict[str, Any]: + """ + 一键停止所有账号交易进程(supervisor)。 + """ + try: + prefix = (prefix or "auto_sys_acc").strip() + if not prefix: + prefix = "auto_sys_acc" + + # 先读取全量 status,拿到有哪些进程 + status_all = _run_supervisorctl(["status"]) + names = _list_supervisor_process_names(status_all) + + targets: list[str] = [] + for n in names: + if n.startswith(prefix): + targets.append(n) + + if include_default: + default_prog = _get_program_name() + if default_prog and default_prog not in targets and default_prog in names: + targets.append(default_prog) + + if not targets: + return { + "message": "未找到可停止的交易进程", + "prefix": prefix, + "include_default": include_default, + "count": 0, + "targets": [], + "status_all": status_all, + } + + results: list[Dict[str, Any]] = [] + ok = 0 + failed = 0 + for prog in targets: + try: + out = _run_supervisorctl(["stop", prog]) + raw = _run_supervisorctl(["status", prog]) + running, pid, state = _parse_supervisor_status(raw) + results.append( + { + "program": prog, + "ok": True, + "output": out, + "status": {"running": running, "pid": pid, "state": state, "raw": raw}, + } + ) + ok += 1 + except Exception as e: + failed += 1 + results.append({"program": prog, "ok": False, "error": str(e)}) + + return { + "message": "已发起批量停止", + "prefix": prefix, + "include_default": include_default, + "count": len(targets), + "ok": ok, + "failed": failed, + "targets": targets, + "results": results, + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"批量停止失败: {e}") + + @router.post("/trading/restart-all") async def trading_restart_all( _admin: Dict[str, Any] = Depends(require_system_admin), diff --git a/frontend/src/components/GlobalConfig.css b/frontend/src/components/GlobalConfig.css index 4841c8a..4dc17c5 100644 --- a/frontend/src/components/GlobalConfig.css +++ b/frontend/src/components/GlobalConfig.css @@ -165,3 +165,125 @@ background: #ffebee; color: #c62828; } + +/* System Control Section */ +.system-section { + border-top: 4px solid #1976d2; +} + +.system-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.system-header h3 { + margin: 0; +} + +.system-status { + display: flex; + align-items: center; + gap: 12px; +} + +.system-status-badge { + padding: 4px 10px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; +} + +.system-status-badge.running { + background: #e8f5e9; + color: #2e7d32; + border: 1px solid #c8e6c9; +} + +.system-status-badge.stopped { + background: #ffebee; + color: #c62828; + border: 1px solid #ffcdd2; +} + +.system-status-meta { + font-size: 12px; + color: #666; + font-family: monospace; +} + +.system-control-group { + background: #f8f9fa; + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; + border: 1px solid #eee; +} + +.control-group-title { + margin: 0 0 12px 0; + font-size: 14px; + font-weight: 600; + color: #555; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.system-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.system-btn { + padding: 8px 16px; + border: 1px solid #ddd; + background: white; + border-radius: 6px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + color: #333; +} + +.system-btn:hover:not(:disabled) { + background: #f5f5f5; + border-color: #bbb; +} + +.system-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.system-btn.primary { + background: #1976d2; + color: white; + border-color: #1565c0; +} + +.system-btn.primary:hover:not(:disabled) { + background: #1565c0; +} + +.system-btn.danger { + background: #dc3545; + color: white; + border-color: #dc3545; +} + +.system-btn.danger:hover:not(:disabled) { + background: #c82333; + border-color: #bd2130; +} + +.system-hint { + font-size: 13px; + color: #666; + margin-top: 12px; + padding: 8px 12px; + background: #fff3e0; + border-radius: 4px; + border: 1px solid #ffe0b2; +} diff --git a/frontend/src/components/GlobalConfig.jsx b/frontend/src/components/GlobalConfig.jsx index 6b7f02f..7ad4d22 100644 --- a/frontend/src/components/GlobalConfig.jsx +++ b/frontend/src/components/GlobalConfig.jsx @@ -212,6 +212,10 @@ const GlobalConfig = () => { const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false) const [snapshotBusy, setSnapshotBusy] = useState(false) + // 新增:搜索和 Tab 状态 + const [searchTerm, setSearchTerm] = useState('') + const [activeTab, setActiveTab] = useState('all') + const PCT_LIKE_KEYS = new Set([ 'LIMIT_ORDER_OFFSET_PCT', 'ENTRY_MAX_DRIFT_PCT_TRENDING', @@ -679,6 +683,21 @@ const GlobalConfig = () => { } } + const handleStopAllTrading = async () => { + if (!window.confirm('确定要停止【所有账号】的交易进程吗?所有账号将停止自动交易!')) return + setSystemBusy(true) + setMessage('') + try { + const res = await api.stopAllTradingSystems({ prefix: 'auto_sys_acc', include_default: true }) + setMessage(`已发起批量停止:共 ${res.count} 个,成功 ${res.ok},失败 ${res.failed}`) + await loadSystemStatus() + } catch (e) { + setMessage('批量停止失败: ' + (e?.message || '未知错误')) + } finally { + setSystemBusy(false) + } + } + const applyPreset = async (presetKey) => { const preset = presets[presetKey] @@ -761,6 +780,57 @@ const GlobalConfig = () => { } } + // 单个配置更新(带确认) + const handleConfigUpdate = async (key, value, config) => { + // 检查是否有实质变化 + if (config.value === value) return + + // 格式化显示值用于确认弹窗 + let displayOld = config.value + let displayNew = value + + // 如果是百分比相关的,尝试转回百分数显示,更直观 + const isPercent = key.includes('PERCENT') || key.includes('PCT') + if (isPercent && typeof value === 'number' && Math.abs(value) <= 1) { + // 简单判断:如果原值是小数且 key 含 PERCENT,可能是小数存储。 + // 这里为了保险,直接显示原始值和新值,管理员自己判断。 + // 或者简单转一下 + displayOld = `${config.value} (${(config.value * 100).toFixed(2)}%)` + displayNew = `${value} (${(value * 100).toFixed(2)}%)` + } + + const confirmMsg = `确定修改配置项【${key}】吗?\n\n原值: ${config.value}\n新值: ${value}\n\n修改将立即生效。` + if (!window.confirm(confirmMsg)) { + // 如果用户取消,理论上 UI 应该回滚。 + // 但 ConfigItem 内部状态已经变了(onBlur 触发)。 + // 触发一次重载配置可以强制 UI 回滚 + await loadConfigs() + return + } + + try { + setSaving(true) + setMessage('') + if (!isAdmin) { + setMessage('只有管理员可以修改全局配置') + return + } + await api.updateGlobalConfigsBatch([{ + key, + value, + type: config.type, + category: config.category, + description: config.description + }]) + setMessage(`已更新 ${key}`) + await loadConfigs() + } catch (error) { + setMessage('更新配置失败: ' + error.message) + } finally { + setSaving(false) + } + } + // 配置快照函数 const isSecretKey = (key) => { return key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET' @@ -1110,78 +1180,85 @@ const GlobalConfig = () => {