This commit is contained in:
薇薇安 2026-01-21 13:09:10 +08:00
parent 11e6361235
commit c28400e51e

View File

@ -351,12 +351,16 @@ const ConfigPanel = ({ currentUser }) => {
useEffect(() => { useEffect(() => {
loadConfigs() loadConfigs()
checkFeasibility() checkFeasibility()
loadSystemStatus() if (isAdmin) {
loadSystemStatus()
}
loadBackendStatus() loadBackendStatus()
loadAccountTradingStatus() loadAccountTradingStatus()
const timer = setInterval(() => { const timer = setInterval(() => {
loadSystemStatus() if (isAdmin) {
loadSystemStatus()
}
loadBackendStatus() loadBackendStatus()
loadAccountTradingStatus() loadAccountTradingStatus()
}, 3000) }, 3000)
@ -525,21 +529,21 @@ const ConfigPanel = ({ currentUser }) => {
preset_detected: detectCurrentPreset(), preset_detected: detectCurrentPreset(),
system_status: systemStatus system_status: systemStatus
? { ? {
running: !!systemStatus.running, running: !!systemStatus.running,
pid: systemStatus.pid || null, pid: systemStatus.pid || null,
program: systemStatus.program || null, program: systemStatus.program || null,
state: systemStatus.state || null, state: systemStatus.state || null,
} }
: null, : null,
feasibility_check_summary: feasibilityCheck feasibility_check_summary: feasibilityCheck
? { ? {
feasible: !!feasibilityCheck.feasible, feasible: !!feasibilityCheck.feasible,
account_balance: feasibilityCheck.account_balance ?? null, account_balance: feasibilityCheck.account_balance ?? null,
base_leverage: feasibilityCheck.base_leverage ?? feasibilityCheck.leverage ?? null, base_leverage: feasibilityCheck.base_leverage ?? feasibilityCheck.leverage ?? null,
max_leverage: feasibilityCheck.max_leverage ?? null, max_leverage: feasibilityCheck.max_leverage ?? null,
use_dynamic_leverage: feasibilityCheck.use_dynamic_leverage ?? null, use_dynamic_leverage: feasibilityCheck.use_dynamic_leverage ?? null,
min_margin_usdt: feasibilityCheck.current_config?.min_margin_usdt ?? null, min_margin_usdt: feasibilityCheck.current_config?.min_margin_usdt ?? null,
} }
: null, : null,
configs: entries, configs: entries,
} }
@ -795,343 +799,343 @@ const ConfigPanel = ({ currentUser }) => {
{/* 系统控制:清缓存 / 启停 / 重启supervisor */} {/* 系统控制:清缓存 / 启停 / 重启supervisor */}
{isAdmin ? ( {isAdmin ? (
<div className="system-section"> <div className="system-section">
<div className="system-header"> <div className="system-header">
<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>
{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={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> </span>
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null} {backendStatus?.pid ? <span className="system-status-meta">PID: {backendStatus.pid}</span> : null}
{systemStatus?.program ? <span className="system-status-meta">程序: {systemStatus.program}</span> : null} {backendStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {backendStatus.meta.requested_at}</span> : null}
{systemStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {systemStatus.meta.requested_at}</span> : null} </div>
<div className="system-hint">
建议流程先更新配置里的 Key 点击清除缓存 点击重启交易系统确保不再使用旧账号下单
</div> </div>
</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={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>
</div>
) : null} ) : null}
{/* 账号管理(超管) */} {/* 账号管理(超管) */}
{isAdmin ? ( {isAdmin ? (
<div className="accounts-admin-section"> <div className="accounts-admin-section">
<div className="accounts-admin-header"> <div className="accounts-admin-header">
<h3>账号管理多账号</h3> <h3>账号管理多账号</h3>
<div className="accounts-admin-actions"> <div className="accounts-admin-actions">
<button <button
type="button" type="button"
className="system-btn" className="system-btn"
onClick={async () => { onClick={async () => {
setAccountsBusy(true) setAccountsBusy(true)
try { try {
await loadAccountsAdmin() await loadAccountsAdmin()
setShowAccountsAdmin((v) => !v) setShowAccountsAdmin((v) => !v)
} finally { } finally {
setAccountsBusy(false) setAccountsBusy(false)
} }
}} }}
disabled={accountsBusy} disabled={accountsBusy}
title="创建/禁用账号;为每个账号配置独立 API KEY/SECRET交易/配置/统计会按账号隔离" title="创建/禁用账号;为每个账号配置独立 API KEY/SECRET交易/配置/统计会按账号隔离"
> >
{showAccountsAdmin ? '收起' : '管理账号'} {showAccountsAdmin ? '收起' : '管理账号'}
</button> </button>
<button <button
type="button" type="button"
className="system-btn" className="system-btn"
onClick={async () => { onClick={async () => {
setAccountsBusy(true) setAccountsBusy(true)
try { try {
await loadAccountsAdmin() await loadAccountsAdmin()
notifyAccountsUpdated() notifyAccountsUpdated()
setMessage('账号列表已刷新') setMessage('账号列表已刷新')
} finally { } finally {
setAccountsBusy(false) setAccountsBusy(false)
} }
}} }}
disabled={accountsBusy} disabled={accountsBusy}
> >
刷新 刷新
</button> </button>
</div>
</div> </div>
</div>
{showAccountsAdmin ? ( {showAccountsAdmin ? (
<div className="accounts-admin-body"> <div className="accounts-admin-body">
<div className="accounts-admin-card">
<div className="accounts-admin-card-title">新增账号</div>
<div className="accounts-form">
<label>
名称
<input
type="text"
value={newAccount.name}
onChange={(e) => setNewAccount({ ...newAccount, name: e.target.value })}
placeholder="例如user_a"
/>
</label>
<label>
API KEY
<input
type="password"
value={newAccount.api_key}
onChange={(e) => setNewAccount({ ...newAccount, api_key: e.target.value })}
placeholder="可先留空,后续再填"
/>
</label>
<label>
API SECRET
<input
type="password"
value={newAccount.api_secret}
onChange={(e) => setNewAccount({ ...newAccount, api_secret: e.target.value })}
placeholder="可先留空,后续再填"
/>
</label>
<label className="accounts-inline">
<span>测试网</span>
<input
type="checkbox"
checked={!!newAccount.use_testnet}
onChange={(e) => setNewAccount({ ...newAccount, use_testnet: e.target.checked })}
/>
</label>
<label>
状态
<select
value={newAccount.status}
onChange={(e) => setNewAccount({ ...newAccount, status: e.target.value })}
>
<option value="active">启用</option>
<option value="disabled">禁用</option>
</select>
</label>
<div className="accounts-form-actions">
<button
type="button"
className="system-btn primary"
disabled={accountsBusy || !newAccount.name.trim()}
onClick={async () => {
setAccountsBusy(true)
setMessage('')
try {
await api.createAccount(newAccount)
setMessage('账号已创建')
setNewAccount({ name: '', api_key: '', api_secret: '', use_testnet: false, status: 'active' })
await loadAccountsAdmin()
notifyAccountsUpdated()
} catch (e) {
setMessage('创建账号失败: ' + (e?.message || '未知错误'))
} finally {
setAccountsBusy(false)
}
}}
>
创建账号
</button>
</div>
</div>
</div>
<div className="accounts-admin-card">
<div className="accounts-admin-card-title">账号列表</div>
<div className="accounts-table">
{(accountsAdmin || []).length ? (
<table>
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>状态</th>
<th>测试网</th>
<th>API KEY</th>
<th>SECRET</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{accountsAdmin.map((a) => (
<tr key={a.id}>
<td>#{a.id}</td>
<td>{a.name || '-'}</td>
<td>
<span className={`acct-badge ${a.status === 'active' ? 'ok' : 'off'}`}>
{a.status === 'active' ? '启用' : '禁用'}
</span>
</td>
<td>{a.use_testnet ? '是' : '否'}</td>
<td>{a.api_key_masked || (a.has_api_key ? '已配置' : '未配置')}</td>
<td>{a.has_api_secret ? '已配置' : '未配置'}</td>
<td className="accounts-actions-cell">
<button
type="button"
className="system-btn"
disabled={accountsBusy || a.id === 1}
title={a.id === 1 ? '默认账号建议保留' : '切换启用/禁用'}
onClick={async () => {
setAccountsBusy(true)
setMessage('')
try {
const next = a.status === 'active' ? 'disabled' : 'active'
await api.updateAccount(a.id, { status: next })
await loadAccountsAdmin()
notifyAccountsUpdated()
setMessage(`账号 #${a.id}${next === 'active' ? '启用' : '禁用'}`)
} catch (e) {
setMessage('更新账号失败: ' + (e?.message || '未知错误'))
} finally {
setAccountsBusy(false)
}
}}
>
{a.status === 'active' ? '禁用' : '启用'}
</button>
<button
type="button"
className="system-btn"
disabled={accountsBusy}
onClick={() => {
setCredEditId(a.id)
setCredForm({ api_key: '', api_secret: '', use_testnet: !!a.use_testnet })
}}
>
更新密钥
</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="accounts-empty">暂无账号默认账号 #1 会自动存在</div>
)}
</div>
</div>
{credEditId ? (
<div className="accounts-admin-card"> <div className="accounts-admin-card">
<div className="accounts-admin-card-title">新账号 #{credEditId} 的密钥</div> <div className="accounts-admin-card-title">新增账号</div>
<div className="accounts-form"> <div className="accounts-form">
<label> <label>
API KEY留空=不改 名称
<input <input
type="password" type="text"
value={credForm.api_key} value={newAccount.name}
onChange={(e) => setCredForm({ ...credForm, api_key: e.target.value })} onChange={(e) => setNewAccount({ ...newAccount, name: e.target.value })}
placeholder="例如user_a"
/> />
</label> </label>
<label> <label>
API SECRET留空=不改 API KEY
<input <input
type="password" type="password"
value={credForm.api_secret} value={newAccount.api_key}
onChange={(e) => setCredForm({ ...credForm, api_secret: e.target.value })} onChange={(e) => setNewAccount({ ...newAccount, api_key: e.target.value })}
placeholder="可先留空,后续再填"
/>
</label>
<label>
API SECRET
<input
type="password"
value={newAccount.api_secret}
onChange={(e) => setNewAccount({ ...newAccount, api_secret: e.target.value })}
placeholder="可先留空,后续再填"
/> />
</label> </label>
<label className="accounts-inline"> <label className="accounts-inline">
<span>测试网</span> <span>测试网</span>
<input <input
type="checkbox" type="checkbox"
checked={!!credForm.use_testnet} checked={!!newAccount.use_testnet}
onChange={(e) => setCredForm({ ...credForm, use_testnet: e.target.checked })} onChange={(e) => setNewAccount({ ...newAccount, use_testnet: e.target.checked })}
/> />
</label> </label>
<label>
状态
<select
value={newAccount.status}
onChange={(e) => setNewAccount({ ...newAccount, status: e.target.value })}
>
<option value="active">启用</option>
<option value="disabled">禁用</option>
</select>
</label>
<div className="accounts-form-actions"> <div className="accounts-form-actions">
<button
type="button"
className="system-btn"
disabled={accountsBusy}
onClick={() => setCredEditId(null)}
>
取消
</button>
<button <button
type="button" type="button"
className="system-btn primary" className="system-btn primary"
disabled={accountsBusy} disabled={accountsBusy || !newAccount.name.trim()}
onClick={async () => { onClick={async () => {
setAccountsBusy(true) setAccountsBusy(true)
setMessage('') setMessage('')
try { try {
const payload = {} await api.createAccount(newAccount)
if (credForm.api_key) payload.api_key = credForm.api_key setMessage('账号已创建')
if (credForm.api_secret) payload.api_secret = credForm.api_secret setNewAccount({ name: '', api_key: '', api_secret: '', use_testnet: false, status: 'active' })
payload.use_testnet = !!credForm.use_testnet
await api.updateAccountCredentials(credEditId, payload)
setMessage(`账号 #${credEditId} 密钥已更新(建议重启该账号交易进程)`)
setCredEditId(null)
await loadAccountsAdmin() await loadAccountsAdmin()
notifyAccountsUpdated() notifyAccountsUpdated()
} catch (e) { } catch (e) {
setMessage('更新密钥失败: ' + (e?.message || '未知错误')) setMessage('创建账号失败: ' + (e?.message || '未知错误'))
} finally { } finally {
setAccountsBusy(false) setAccountsBusy(false)
} }
}} }}
> >
保存 创建账号
</button> </button>
</div> </div>
</div> </div>
</div> </div>
) : null}
</div> <div className="accounts-admin-card">
) : null} <div className="accounts-admin-card-title">账号列表</div>
</div> <div className="accounts-table">
{(accountsAdmin || []).length ? (
<table>
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>状态</th>
<th>测试网</th>
<th>API KEY</th>
<th>SECRET</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{accountsAdmin.map((a) => (
<tr key={a.id}>
<td>#{a.id}</td>
<td>{a.name || '-'}</td>
<td>
<span className={`acct-badge ${a.status === 'active' ? 'ok' : 'off'}`}>
{a.status === 'active' ? '启用' : '禁用'}
</span>
</td>
<td>{a.use_testnet ? '是' : '否'}</td>
<td>{a.api_key_masked || (a.has_api_key ? '已配置' : '未配置')}</td>
<td>{a.has_api_secret ? '已配置' : '未配置'}</td>
<td className="accounts-actions-cell">
<button
type="button"
className="system-btn"
disabled={accountsBusy || a.id === 1}
title={a.id === 1 ? '默认账号建议保留' : '切换启用/禁用'}
onClick={async () => {
setAccountsBusy(true)
setMessage('')
try {
const next = a.status === 'active' ? 'disabled' : 'active'
await api.updateAccount(a.id, { status: next })
await loadAccountsAdmin()
notifyAccountsUpdated()
setMessage(`账号 #${a.id}${next === 'active' ? '启用' : '禁用'}`)
} catch (e) {
setMessage('更新账号失败: ' + (e?.message || '未知错误'))
} finally {
setAccountsBusy(false)
}
}}
>
{a.status === 'active' ? '禁用' : '启用'}
</button>
<button
type="button"
className="system-btn"
disabled={accountsBusy}
onClick={() => {
setCredEditId(a.id)
setCredForm({ api_key: '', api_secret: '', use_testnet: !!a.use_testnet })
}}
>
更新密钥
</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="accounts-empty">暂无账号默认账号 #1 会自动存在</div>
)}
</div>
</div>
{credEditId ? (
<div className="accounts-admin-card">
<div className="accounts-admin-card-title">更新账号 #{credEditId} 的密钥</div>
<div className="accounts-form">
<label>
API KEY留空=不改
<input
type="password"
value={credForm.api_key}
onChange={(e) => setCredForm({ ...credForm, api_key: e.target.value })}
/>
</label>
<label>
API SECRET留空=不改
<input
type="password"
value={credForm.api_secret}
onChange={(e) => setCredForm({ ...credForm, api_secret: e.target.value })}
/>
</label>
<label className="accounts-inline">
<span>测试网</span>
<input
type="checkbox"
checked={!!credForm.use_testnet}
onChange={(e) => setCredForm({ ...credForm, use_testnet: e.target.checked })}
/>
</label>
<div className="accounts-form-actions">
<button
type="button"
className="system-btn"
disabled={accountsBusy}
onClick={() => setCredEditId(null)}
>
取消
</button>
<button
type="button"
className="system-btn primary"
disabled={accountsBusy}
onClick={async () => {
setAccountsBusy(true)
setMessage('')
try {
const payload = {}
if (credForm.api_key) payload.api_key = credForm.api_key
if (credForm.api_secret) payload.api_secret = credForm.api_secret
payload.use_testnet = !!credForm.use_testnet
await api.updateAccountCredentials(credEditId, payload)
setMessage(`账号 #${credEditId} 密钥已更新(建议重启该账号交易进程)`)
setCredEditId(null)
await loadAccountsAdmin()
notifyAccountsUpdated()
} catch (e) {
setMessage('更新密钥失败: ' + (e?.message || '未知错误'))
} finally {
setAccountsBusy(false)
}
}}
>
保存
</button>
</div>
</div>
</div>
) : null}
</div>
) : null}
</div>
) : null} ) : null}
{/* 预设方案快速切换 */} {/* 预设方案快速切换 */}