This commit is contained in:
薇薇安 2026-02-03 16:03:43 +08:00
parent 9f21cc1d02
commit efc88b2083
2 changed files with 288 additions and 359 deletions

View File

@ -126,49 +126,259 @@ const UserAccountGroup = ({ user, allAccounts, onServiceAction }) => {
</td>
<td>
{acc.serviceStatus ? (
<span className={`status-badge`}
style={{
backgroundColor: acc.serviceStatus.running ? '#e8f5e9' : '#ffebee',
color: acc.serviceStatus.running ? '#2e7d32' : '#c62828',
border: `1px solid ${acc.serviceStatus.running ? '#c8e6c9' : '#ffcdd2'}`,
padding: '2px 8px',
borderRadius: '12px',
fontSize: '12px'
}}>
{acc.serviceStatus.state}
</span>
<span className={`status-badge ${acc.serviceStatus.running ? 'running' : 'stopped'}`}>
{acc.serviceStatus.running ? '运行中' : '停止'}
</span>
) : (
<span style={{ color: '#999' }}>UNKNOWN</span>
<span className="status-badge stopped">未启动</span>
)}
</td>
<td>{Number(acc.total_balance).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
<td className={Number(acc.total_pnl) >= 0 ? 'profit' : 'loss'}>
{Number(acc.total_pnl) > 0 ? '+' : ''}{Number(acc.total_pnl).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
<td>{acc.total_balance?.toFixed(2) || '-'}</td>
<td className={acc.total_pnl >= 0 ? 'profit' : 'loss'}>
{acc.total_pnl?.toFixed(2) || '-'}
</td>
<td>{acc.open_positions}</td>
<td>{acc.open_positions || 0}</td>
<td>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => onServiceAction(acc.id, 'start')}
disabled={acc.serviceStatus?.running}
className="btn-mini start"
<button
className="btn-icon"
onClick={() => handleServiceAction(acc.id, acc.serviceStatus?.running ? 'stop' : 'start')}
title={acc.serviceStatus?.running ? '停止服务' : '启动服务'}
>
{acc.serviceStatus?.running ? '⏹' : '▶'}
</button>
<button
className="btn-icon danger"
onClick={() => handleRevoke(acc.id)}
title="取消关联"
style={{ marginLeft: '8px' }}
>
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
<div className="add-account-section">
<h4>新增关联</h4>
<div className="add-account-form">
<select
value={linkAccountId}
onChange={e => setLinkAccountId(e.target.value)}
disabled={associating}
>
<option value="">选择账号...</option>
{availableAccounts.map(a => (
<option key={a.id} value={a.id}>
#{a.id} {a.name} ({a.status === 'active' ? '启用' : '禁用'})
</option>
))}
</select>
<select
value={linkRole}
onChange={e => setLinkRole(e.target.value)}
disabled={associating}
>
<option value="viewer">观察者 (Viewer)</option>
<option value="trader">交易员 (Trader)</option>
</select>
<button
onClick={handleGrant}
disabled={!linkAccountId || associating}
className="btn-primary"
>
{associating ? '关联中...' : '关联'}
</button>
</div>
</div>
</div>
)}
</div>
)
}
const AccountManager = ({ accounts, onRefresh }) => {
const [newAccount, setNewAccount] = useState({ name: '', api_key: '', api_secret: '', use_testnet: false, status: 'active' })
const [credEditId, setCredEditId] = useState(null)
const [credForm, setCredForm] = useState({ api_key: '', api_secret: '', use_testnet: false })
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState('')
const notifyAccountsUpdated = () => {
try {
window.dispatchEvent(new Event('ats:accounts:updated'))
} catch (e) {
// ignore
}
}
const handleCreate = async () => {
if (!newAccount.name.trim()) return
setBusy(true)
setMessage('')
try {
await api.createAccount(newAccount)
setMessage('账号已创建')
setNewAccount({ name: '', api_key: '', api_secret: '', use_testnet: false, status: 'active' })
if (onRefresh) onRefresh()
notifyAccountsUpdated()
} catch (e) {
setMessage('创建账号失败: ' + (e?.message || '未知错误'))
} finally {
setBusy(false)
}
}
const handleUpdateStatus = async (account) => {
setBusy(true)
setMessage('')
try {
const next = account.status === 'active' ? 'disabled' : 'active'
await api.updateAccount(account.id, { status: next })
setMessage(`账号 #${account.id}${next === 'active' ? '启用' : '禁用'}`)
if (onRefresh) onRefresh()
notifyAccountsUpdated()
} catch (e) {
setMessage('更新账号失败: ' + (e?.message || '未知错误'))
} finally {
setBusy(false)
}
}
const handleUpdateCreds = async () => {
if (!credEditId) return
setBusy(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)
if (onRefresh) onRefresh()
notifyAccountsUpdated()
} catch (e) {
setMessage('更新密钥失败: ' + (e?.message || '未知错误'))
} finally {
setBusy(false)
}
}
return (
<div className="accounts-manager-section">
<h3>系统账号池管理</h3>
{message && <div className={`message ${message.includes('失败') ? 'error' : 'success'}`}>{message}</div>}
<div className="accounts-manager-grid">
{/* Create Account Card */}
<div className="card create-account-card">
<h4>新增账号</h4>
<div className="form-group">
<label>名称</label>
<input
type="text"
value={newAccount.name}
onChange={(e) => setNewAccount({ ...newAccount, name: e.target.value })}
placeholder="例如user_a"
/>
</div>
<div className="form-group">
<label>API KEY (可选)</label>
<input
type="password"
value={newAccount.api_key}
onChange={(e) => setNewAccount({ ...newAccount, api_key: e.target.value })}
/>
</div>
<div className="form-group">
<label>API SECRET (可选)</label>
<input
type="password"
value={newAccount.api_secret}
onChange={(e) => setNewAccount({ ...newAccount, api_secret: e.target.value })}
/>
</div>
<div className="form-group checkbox">
<label>
<input
type="checkbox"
checked={!!newAccount.use_testnet}
onChange={(e) => setNewAccount({ ...newAccount, use_testnet: e.target.checked })}
/>
测试网
</label>
</div>
<div className="form-group">
<label>初始状态</label>
<select
value={newAccount.status}
onChange={(e) => setNewAccount({ ...newAccount, status: e.target.value })}
>
<option value="active">启用</option>
<option value="disabled">禁用</option>
</select>
</div>
<button
className="btn-primary full-width"
onClick={handleCreate}
disabled={busy || !newAccount.name.trim()}
>
创建账号
</button>
</div>
{/* Account List Card */}
<div className="card account-list-card">
<h4>账号列表 ({accounts?.length || 0})</h4>
<div className="table-wrapper">
<table className="data-table">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>状态</th>
<th>测试网</th>
<th>API配置</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{(accounts || []).map(a => (
<tr key={a.id}>
<td>#{a.id}</td>
<td>{a.name}</td>
<td>
<span className={`status-badge ${a.status}`}>
{a.status === 'active' ? '启用' : '禁用'}
</span>
</td>
<td>{a.use_testnet ? '是' : '否'}</td>
<td>
{a.has_api_key ? '✅' : '❌'} / {a.has_api_secret ? '✅' : '❌'}
</td>
<td>
<div className="action-buttons">
<button
className="btn-sm"
disabled={busy || a.id === 1}
onClick={() => handleUpdateStatus(a)}
title={a.id === 1 ? '默认账号不可禁用' : '切换状态'}
>
启动
</button>
<button
onClick={() => onServiceAction(acc.id, 'stop')}
disabled={!acc.serviceStatus?.running}
className="btn-mini stop"
>
停止
{a.status === 'active' ? '禁用' : '启用'}
</button>
<button
onClick={() => handleRevoke(acc.id)}
className="btn-mini danger"
style={{ marginLeft: '4px' }}
title="取消关联"
className="btn-sm"
disabled={busy}
onClick={() => {
setCredEditId(a.id)
setCredForm({ api_key: '', api_secret: '', use_testnet: !!a.use_testnet })
}}
>
密钥
</button>
</div>
</td>
@ -176,37 +386,45 @@ const UserAccountGroup = ({ user, allAccounts, onServiceAction }) => {
))}
</tbody>
</table>
)}
</div>
</div>
</div>
{/* 关联管理区域 */}
<div className="association-controls" style={{ marginTop: '15px', padding: '12px', backgroundColor: '#f8f9fa', borderRadius: '6px', border: '1px solid #e9ecef', display: 'flex', gap: '10px', alignItems: 'center' }}>
<span style={{ fontSize: '13px', fontWeight: 'bold', color: '#495057' }}>新增关联:</span>
<select
value={linkAccountId}
onChange={e => setLinkAccountId(e.target.value)}
style={{ padding: '4px 8px', borderRadius: '4px', border: '1px solid #ced4da' }}
>
<option value="">选择账号...</option>
{availableAccounts.map(a => (
<option key={a.id} value={a.id}>{a.name || `Account ${a.id}`}</option>
))}
</select>
<select
value={linkRole}
onChange={e => setLinkRole(e.target.value)}
style={{ padding: '4px 8px', borderRadius: '4px', border: '1px solid #ced4da' }}
>
<option value="viewer">观察者 (Viewer)</option>
<option value="trader">交易员 (Trader)</option>
</select>
<button
onClick={handleGrant}
disabled={!linkAccountId || associating}
className="btn-mini primary"
style={{ padding: '4px 12px' }}
>
关联
</button>
{/* Credential Edit Modal/Overlay */}
{credEditId && (
<div className="modal-overlay">
<div className="modal-content">
<h4>更新密钥 (账号 #{credEditId})</h4>
<div className="form-group">
<label>API KEY (留空=不改)</label>
<input
type="password"
value={credForm.api_key}
onChange={(e) => setCredForm({ ...credForm, api_key: e.target.value })}
/>
</div>
<div className="form-group">
<label>API SECRET (留空=不改)</label>
<input
type="password"
value={credForm.api_secret}
onChange={(e) => setCredForm({ ...credForm, api_secret: e.target.value })}
/>
</div>
<div className="form-group checkbox">
<label>
<input
type="checkbox"
checked={!!credForm.use_testnet}
onChange={(e) => setCredForm({ ...credForm, use_testnet: e.target.checked })}
/>
测试网
</label>
</div>
<div className="modal-actions">
<button className="btn-secondary" onClick={() => setCredEditId(null)}>取消</button>
<button className="btn-primary" onClick={handleUpdateCreds} disabled={busy}>保存</button>
</div>
</div>
</div>
)}
@ -354,6 +572,11 @@ const AdminDashboard = () => {
))}
</div>
</div>
<AccountManager
accounts={data.allAccounts}
onRefresh={loadData}
/>
</div>
)
}

View File

@ -42,21 +42,9 @@ const ConfigPanel = () => {
}
}
//
const [accountsAdmin, setAccountsAdmin] = useState([])
const [accountsBusy, setAccountsBusy] = useState(false)
const [showAccountsAdmin, setShowAccountsAdmin] = useState(false)
const [newAccount, setNewAccount] = useState({
name: '',
api_key: '',
api_secret: '',
use_testnet: false,
status: 'active',
})
const [credEditId, setCredEditId] = useState(null)
const [credForm, setCredForm] = useState({ api_key: '', api_secret: '', use_testnet: false })
// PCT<=1<=1%0~1
//
// LIMIT_ORDER_OFFSET_PCT=0.5 0.5% 50%
const PCT_LIKE_KEYS = new Set([
'LIMIT_ORDER_OFFSET_PCT',
@ -489,23 +477,6 @@ const ConfigPanel = () => {
// accountIdonChanged
const loadAccountsAdmin = async () => {
try {
const list = await api.getAccounts()
setAccountsAdmin(Array.isArray(list) ? list : [])
} catch (e) {
setAccountsAdmin([])
}
}
const notifyAccountsUpdated = () => {
try {
window.dispatchEvent(new Event('ats:accounts:updated'))
} catch (e) {
// ignore
}
}
// accountId useEffect
const checkFeasibility = async () => {
@ -1041,272 +1012,7 @@ const ConfigPanel = () => {
</div>
) : null}
{/* 账号管理(超管) */}
{isAdmin ? (
<div className="accounts-admin-section">
<div className="accounts-admin-header">
<h3>账号管理多账号</h3>
<div className="accounts-admin-actions">
<button
type="button"
className="system-btn"
onClick={async () => {
setAccountsBusy(true)
try {
await loadAccountsAdmin()
setShowAccountsAdmin((v) => !v)
} finally {
setAccountsBusy(false)
}
}}
disabled={accountsBusy}
title="创建/禁用账号;为每个账号配置独立 API KEY/SECRET交易/配置/统计会按账号隔离"
>
{showAccountsAdmin ? '收起' : '管理账号'}
</button>
<button
type="button"
className="system-btn"
onClick={async () => {
setAccountsBusy(true)
try {
await loadAccountsAdmin()
notifyAccountsUpdated()
setMessage('账号列表已刷新')
} finally {
setAccountsBusy(false)
}
}}
disabled={accountsBusy}
>
刷新
</button>
</div>
</div>
{showAccountsAdmin ? (
<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-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}
{/* 用户提示 */}
{!isAdmin && (