a
This commit is contained in:
parent
0a0bcd941b
commit
c01f681dec
|
|
@ -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}")
|
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")
|
@router.post("/trading/restart-all")
|
||||||
async def trading_restart_all(
|
async def trading_restart_all(
|
||||||
_admin: Dict[str, Any] = Depends(require_system_admin),
|
_admin: Dict[str, Any] = Depends(require_system_admin),
|
||||||
|
|
|
||||||
|
|
@ -165,3 +165,125 @@
|
||||||
background: #ffebee;
|
background: #ffebee;
|
||||||
color: #c62828;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,10 @@ const GlobalConfig = () => {
|
||||||
const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false)
|
const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false)
|
||||||
const [snapshotBusy, setSnapshotBusy] = useState(false)
|
const [snapshotBusy, setSnapshotBusy] = useState(false)
|
||||||
|
|
||||||
|
// 新增:搜索和 Tab 状态
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [activeTab, setActiveTab] = useState('all')
|
||||||
|
|
||||||
const PCT_LIKE_KEYS = new Set([
|
const PCT_LIKE_KEYS = new Set([
|
||||||
'LIMIT_ORDER_OFFSET_PCT',
|
'LIMIT_ORDER_OFFSET_PCT',
|
||||||
'ENTRY_MAX_DRIFT_PCT_TRENDING',
|
'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 applyPreset = async (presetKey) => {
|
||||||
const preset = presets[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) => {
|
const isSecretKey = (key) => {
|
||||||
return key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET'
|
return key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET'
|
||||||
|
|
@ -1110,78 +1180,85 @@ const GlobalConfig = () => {
|
||||||
<h3>系统控制</h3>
|
<h3>系统控制</h3>
|
||||||
<div className="system-status">
|
<div className="system-status">
|
||||||
<span className={`system-status-badge ${systemStatus?.running ? 'running' : 'stopped'}`}>
|
<span className={`system-status-badge ${systemStatus?.running ? 'running' : 'stopped'}`}>
|
||||||
{systemStatus?.running ? '运行中' : '未运行'}
|
交易系统 {systemStatus?.running ? '运行中' : '未运行'}
|
||||||
</span>
|
</span>
|
||||||
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null}
|
{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}
|
<span className={`system-status-badge ${backendStatus?.running ? 'running' : 'stopped'}`} style={{ marginLeft: '10px' }}>
|
||||||
|
后端服务 {backendStatus?.running ? '运行中' : '未知'}
|
||||||
|
</span>
|
||||||
|
{backendStatus?.pid ? <span className="system-status-meta">PID: {backendStatus.pid}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="system-actions">
|
|
||||||
<button
|
<div className="system-control-group">
|
||||||
type="button"
|
<h4 className="control-group-title" style={{ margin: '10px 0 8px', fontSize: '14px', color: '#666' }}>后端服务管理</h4>
|
||||||
className="system-btn"
|
<div className="system-actions">
|
||||||
onClick={handleClearCache}
|
<button
|
||||||
disabled={systemBusy}
|
type="button"
|
||||||
title="清理Redis配置缓存并从数据库回灌。切换API Key后建议先点这里,再重启交易系统。"
|
className="system-btn primary"
|
||||||
>
|
onClick={handleRestartBackend}
|
||||||
清除缓存
|
disabled={systemBusy}
|
||||||
</button>
|
title="通过 backend/restart.sh 重启后端(uvicorn)。重启期间接口会短暂不可用。"
|
||||||
<button
|
>
|
||||||
type="button"
|
重启后端服务
|
||||||
className="system-btn"
|
</button>
|
||||||
onClick={handleStopTrading}
|
<button
|
||||||
disabled={systemBusy || systemStatus?.running === false}
|
type="button"
|
||||||
title="通过 supervisorctl 停止交易系统"
|
className="system-btn"
|
||||||
>
|
onClick={handleClearCache}
|
||||||
停止
|
disabled={systemBusy}
|
||||||
</button>
|
title="清理Redis配置缓存并从数据库回灌。切换API Key后建议先点这里,再重启交易系统。"
|
||||||
<button
|
>
|
||||||
type="button"
|
清除缓存
|
||||||
className="system-btn"
|
</button>
|
||||||
onClick={handleStartTrading}
|
</div>
|
||||||
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>
|
||||||
<div className="system-status" style={{ marginTop: '10px' }}>
|
|
||||||
<span className={`system-status-badge ${backendStatus?.running ? 'running' : 'stopped'}`}>
|
<div className="system-control-group" style={{ marginTop: '15px' }}>
|
||||||
后端 {backendStatus?.running ? '运行中' : '未知'}
|
<h4 className="control-group-title" style={{ margin: '10px 0 8px', fontSize: '14px', color: '#666' }}>交易服务管理</h4>
|
||||||
</span>
|
<div className="system-actions">
|
||||||
{backendStatus?.pid ? <span className="system-status-meta">PID: {backendStatus.pid}</span> : null}
|
<button
|
||||||
{backendStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {backendStatus.meta.requested_at}</span> : null}
|
type="button"
|
||||||
|
className="system-btn"
|
||||||
|
onClick={handleStartTrading}
|
||||||
|
disabled={systemBusy || systemStatus?.running === true}
|
||||||
|
title="通过 supervisorctl 启动交易系统"
|
||||||
|
>
|
||||||
|
启动
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="system-btn"
|
||||||
|
onClick={handleStopTrading}
|
||||||
|
disabled={systemBusy || systemStatus?.running === false}
|
||||||
|
title="通过 supervisorctl 停止交易系统"
|
||||||
|
>
|
||||||
|
停止
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="system-btn primary"
|
||||||
|
onClick={handleRestartAllTrading}
|
||||||
|
disabled={systemBusy}
|
||||||
|
title="批量重启所有账号交易进程(auto_sys_acc*),用于代码升级后统一生效"
|
||||||
|
>
|
||||||
|
重启所有账号交易
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="system-btn danger"
|
||||||
|
onClick={handleStopAllTrading}
|
||||||
|
disabled={systemBusy}
|
||||||
|
title="批量停止所有账号交易进程(auto_sys_acc*)"
|
||||||
|
>
|
||||||
|
停止所有账号交易
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="system-hint">
|
<div className="system-hint">
|
||||||
建议流程:先更新配置里的 Key → 点击"清除缓存" → 点击"重启交易系统",确保不再使用旧账号下单。
|
建议流程:先更新配置里的 Key → 点击"清除缓存" → 点击"重启所有账号交易",确保不再使用旧账号下单。
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1269,64 +1346,137 @@ const GlobalConfig = () => {
|
||||||
修改全局策略配置,所有普通用户账号将使用这些配置(风险旋钮除外)
|
修改全局策略配置,所有普通用户账号将使用这些配置(风险旋钮除外)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 搜索和筛选栏 */}
|
||||||
|
<div className="config-toolbar" style={{ marginBottom: '20px', display: 'flex', flexDirection: 'column', gap: '15px' }}>
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<div className="search-bar" style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="🔍 搜索配置项 Key、描述或中文名..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 12px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{searchTerm && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchTerm('')}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: '10px',
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#999',
|
||||||
|
fontSize: '16px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="config-tabs" style={{ display: 'flex', gap: '10px', overflowX: 'auto', paddingBottom: '5px' }}>
|
||||||
|
{[
|
||||||
|
{ key: 'all', label: '全部' },
|
||||||
|
{ key: 'risk', label: '风险控制' },
|
||||||
|
{ key: 'strategy', label: '策略参数' },
|
||||||
|
{ key: 'scan', label: '市场扫描' },
|
||||||
|
{ key: 'position', label: '仓位控制' },
|
||||||
|
].map(tab => (
|
||||||
|
<button
|
||||||
|
key={tab.key}
|
||||||
|
onClick={() => setActiveTab(tab.key)}
|
||||||
|
className={`tab-btn ${activeTab === tab.key ? 'active' : ''}`}
|
||||||
|
style={{
|
||||||
|
padding: '8px 16px',
|
||||||
|
borderRadius: '20px',
|
||||||
|
border: activeTab === tab.key ? '1px solid #007bff' : '1px solid #eee',
|
||||||
|
background: activeTab === tab.key ? '#e7f1ff' : '#f9f9f9',
|
||||||
|
color: activeTab === tab.key ? '#007bff' : '#666',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontWeight: activeTab === tab.key ? '600' : 'normal',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{Object.keys(configs).length > 0 ? (
|
{Object.keys(configs).length > 0 ? (
|
||||||
(() => {
|
(() => {
|
||||||
const configCategories = {
|
const configCategories = {
|
||||||
'scan': '市场扫描',
|
|
||||||
'position': '仓位控制',
|
|
||||||
'risk': '风险控制',
|
'risk': '风险控制',
|
||||||
'strategy': '策略参数',
|
'strategy': '策略参数',
|
||||||
|
'scan': '市场扫描',
|
||||||
|
'position': '仓位控制',
|
||||||
}
|
}
|
||||||
return Object.entries(configCategories).map(([category, label]) => {
|
|
||||||
const categoryConfigs = Object.entries(configs).filter(([key, config]) => {
|
// 过滤逻辑
|
||||||
// 确保config是对象且有category字段
|
const filteredConfigs = Object.entries(configs).filter(([key, config]) => {
|
||||||
if (!config || typeof config !== 'object') {
|
// 1. 基础过滤(排除非对象、风险旋钮、API Key)
|
||||||
console.warn(`Config ${key} is not an object:`, config)
|
if (!config || typeof config !== 'object') return false
|
||||||
return false
|
const RISK_KNOBS_KEYS = ['MIN_MARGIN_USDT', 'MIN_POSITION_PERCENT', 'MAX_POSITION_PERCENT',
|
||||||
}
|
'MAX_TOTAL_POSITION_PERCENT', 'AUTO_TRADE_ENABLED', 'MAX_OPEN_POSITIONS', 'MAX_DAILY_ENTRIES']
|
||||||
if (!config.category || config.category !== category) return false
|
if (RISK_KNOBS_KEYS.includes(key)) return false
|
||||||
// 排除风险旋钮(这些由用户自己控制)
|
if (key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET' || key === 'USE_TESTNET') return false
|
||||||
const RISK_KNOBS_KEYS = ['MIN_MARGIN_USDT', 'MIN_POSITION_PERCENT', 'MAX_POSITION_PERCENT',
|
|
||||||
'MAX_TOTAL_POSITION_PERCENT', 'AUTO_TRADE_ENABLED', 'MAX_OPEN_POSITIONS', 'MAX_DAILY_ENTRIES']
|
// 2. Tab 过滤
|
||||||
if (RISK_KNOBS_KEYS.includes(key)) return false
|
if (activeTab !== 'all' && config.category !== activeTab) return false
|
||||||
// 排除API密钥(在账号管理中)
|
|
||||||
if (key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET' || key === 'USE_TESTNET') return false
|
// 3. 搜索过滤
|
||||||
return true
|
if (searchTerm) {
|
||||||
})
|
const lowerTerm = searchTerm.toLowerCase()
|
||||||
if (categoryConfigs.length === 0) return null
|
const matchKey = key.toLowerCase().includes(lowerTerm)
|
||||||
console.log(`Category ${category} (${label}): ${categoryConfigs.length} configs`, categoryConfigs.map(([k]) => k))
|
const matchDesc = (config.description || '').toLowerCase().includes(lowerTerm)
|
||||||
|
const matchLabel = (KEY_LABELS[key] || '').toLowerCase().includes(lowerTerm)
|
||||||
|
if (!matchKey && !matchDesc && !matchLabel) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (filteredConfigs.length === 0) {
|
||||||
|
return <div style={{ textAlign: 'center', color: '#999', padding: '30px', background: '#f5f5f5', borderRadius: '8px' }}>未找到匹配的配置项</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分组渲染
|
||||||
|
// 如果是 'all' Tab,按 Category 分组
|
||||||
|
// 如果是特定 Tab,直接渲染(或者也分组,只有一个组)
|
||||||
|
const groupsToRender = activeTab === 'all'
|
||||||
|
? Object.keys(configCategories)
|
||||||
|
: [activeTab]
|
||||||
|
|
||||||
|
// 将 filteredConfigs 转为 Map 或方便查找的结构,或者每次 filter
|
||||||
|
|
||||||
|
return groupsToRender.map(category => {
|
||||||
|
const categoryLabel = configCategories[category] || category
|
||||||
|
const groupConfigs = filteredConfigs.filter(([_, cfg]) => cfg.category === category)
|
||||||
|
|
||||||
|
if (groupConfigs.length === 0) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={category} style={{ marginBottom: '24px' }}>
|
<div key={category} style={{ marginBottom: '24px' }}>
|
||||||
<h4 style={{ marginBottom: '12px', fontSize: '16px', fontWeight: '600' }}>{label}</h4>
|
<h4 style={{ marginBottom: '12px', fontSize: '16px', fontWeight: '600', borderLeft: '4px solid #007bff', paddingLeft: '10px', color: '#333' }}>
|
||||||
|
{categoryLabel}
|
||||||
|
</h4>
|
||||||
<div className="config-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}>
|
<div className="config-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}>
|
||||||
{categoryConfigs.map(([key, config]) => (
|
{groupConfigs.map(([key, config]) => (
|
||||||
<ConfigItem
|
<ConfigItem
|
||||||
key={key}
|
key={key}
|
||||||
label={key}
|
label={key}
|
||||||
config={config}
|
config={config}
|
||||||
onUpdate={async (value) => {
|
onUpdate={(val) => handleConfigUpdate(key, val, config)}
|
||||||
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)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -612,6 +612,24 @@ export const api = {
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
stopAllTradingSystems: async (opts = {}) => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (opts?.prefix) params.set('prefix', String(opts.prefix))
|
||||||
|
if (opts?.include_default) params.set('include_default', 'true')
|
||||||
|
const url = params.toString()
|
||||||
|
? `${buildUrl('/api/system/trading/stop-all')}?${params.toString()}`
|
||||||
|
: buildUrl('/api/system/trading/stop-all')
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '批量停止失败' }))
|
||||||
|
throw new Error(error.detail || '批量停止失败')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
|
||||||
restartAllTradingSystems: async (opts = {}) => {
|
restartAllTradingSystems: async (opts = {}) => {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (opts?.prefix) params.set('prefix', String(opts.prefix))
|
if (opts?.prefix) params.set('prefix', String(opts.prefix))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user