1
This commit is contained in:
parent
614b28493b
commit
50c933a8b0
|
|
@ -195,6 +195,10 @@ const GlobalConfig = () => {
|
|||
const [showUserForm, setShowUserForm] = useState(false)
|
||||
const [newUser, setNewUser] = useState({ username: '', password: '', role: 'user', status: 'active' })
|
||||
const [editingUserId, setEditingUserId] = useState(null)
|
||||
const [usersDetailed, setUsersDetailed] = useState([])
|
||||
const [accountsAdmin, setAccountsAdmin] = useState([])
|
||||
const [linkRole, setLinkRole] = useState('viewer')
|
||||
const [linkAccountMap, setLinkAccountMap] = useState({})
|
||||
|
||||
// 系统控制相关
|
||||
const [systemStatus, setSystemStatus] = useState(null)
|
||||
|
|
@ -1125,6 +1129,58 @@ const GlobalConfig = () => {
|
|||
},
|
||||
]
|
||||
|
||||
const loadUsersAndAccounts = async () => {
|
||||
if (!isAdmin) return
|
||||
try {
|
||||
setBusy(true)
|
||||
const [users, accounts] = await Promise.all([
|
||||
api.getUsersDetailed ? api.getUsersDetailed() : api.get('/admin/users/detailed').then(r => r.data),
|
||||
api.getAccounts(),
|
||||
])
|
||||
setUsersDetailed(Array.isArray(users) ? users : [])
|
||||
setAccountsAdmin(Array.isArray(accounts) ? accounts : [])
|
||||
const initMap = {}
|
||||
;(Array.isArray(users) ? users : []).forEach(u => {
|
||||
initMap[u.id] = ''
|
||||
})
|
||||
setLinkAccountMap(initMap)
|
||||
} catch (e) {
|
||||
setMessage(e?.message || '加载失败')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (isAdmin) loadUsersAndAccounts()
|
||||
}, [isAdmin])
|
||||
|
||||
const handleGrant = async (userId) => {
|
||||
const aid = parseInt(String(linkAccountMap[userId] || ''), 10)
|
||||
if (!Number.isFinite(aid) || aid <= 0) return
|
||||
try {
|
||||
setBusy(true)
|
||||
await api.grantUserAccount(userId, aid, linkRole)
|
||||
setMessage('已关联账号')
|
||||
await loadUsersAndAccounts()
|
||||
} catch (e) {
|
||||
setMessage(e?.message || '关联失败')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
const handleRevoke = async (userId, accountId) => {
|
||||
try {
|
||||
setBusy(true)
|
||||
await api.revokeUserAccount(userId, accountId)
|
||||
setMessage('已取消关联')
|
||||
await loadUsersAndAccounts()
|
||||
} catch (e) {
|
||||
setMessage(e?.message || '取消失败')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="global-config">
|
||||
<div className="global-config-header">
|
||||
|
|
@ -1250,6 +1306,96 @@ const GlobalConfig = () => {
|
|||
</section>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<section className="global-section">
|
||||
<div className="section-header">
|
||||
<h3>用户管理与账号授权</h3>
|
||||
<p style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>
|
||||
管理用户与其关联的交易账号;用户登录后仅能看到已授权的账号,并可在顶部下拉切换
|
||||
</p>
|
||||
</div>
|
||||
<div className="accounts-table">
|
||||
{usersDetailed.length ? (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户</th>
|
||||
<th>角色</th>
|
||||
<th>状态</th>
|
||||
<th>已关联账号</th>
|
||||
<th>关联新账号</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{usersDetailed.map(u => {
|
||||
const userAccs = Array.isArray(u.accounts) ? u.accounts : []
|
||||
const linkedIds = new Set(userAccs.map(a => a.id))
|
||||
const available = accountsAdmin.filter(a => !linkedIds.has(a.id))
|
||||
return (
|
||||
<tr key={u.id}>
|
||||
<td>{u.username}</td>
|
||||
<td><span className={`acct-badge ${u.role === 'admin' ? 'ok' : 'off'}`}>{u.role}</span></td>
|
||||
<td><span className={`acct-badge ${u.status === 'active' ? 'ok' : 'off'}`}>{u.status === 'active' ? '启用' : '禁用'}</span></td>
|
||||
<td>
|
||||
{userAccs.length === 0 ? (
|
||||
<span style={{ color: '#999' }}>暂无</span>
|
||||
) : (
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
{userAccs.map(a => (
|
||||
<div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: '6px', border: '1px solid #eee', padding: '4px 8px', borderRadius: '6px' }}>
|
||||
<span>#{a.id} {a.name}</span>
|
||||
<span className={`acct-badge ${a.status === 'active' ? 'ok' : 'off'}`}>{a.status === 'active' ? '启用' : '禁用'}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="system-btn danger"
|
||||
onClick={() => handleRevoke(u.id, a.id)}
|
||||
disabled={busy}
|
||||
>
|
||||
取消关联
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<select
|
||||
value={linkAccountMap[u.id] || ''}
|
||||
onChange={(e) => setLinkAccountMap({ ...linkAccountMap, [u.id]: e.target.value })}
|
||||
style={{ minWidth: '200px' }}
|
||||
>
|
||||
<option value="">选择账号</option>
|
||||
{available.map(a => (
|
||||
<option key={a.id} value={a.id}>#{a.id} {a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<select value={linkRole} onChange={(e) => setLinkRole(e.target.value)}>
|
||||
<option value="viewer">viewer</option>
|
||||
<option value="owner">owner</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="system-btn primary"
|
||||
onClick={() => handleGrant(u.id)}
|
||||
disabled={busy || !linkAccountMap[u.id]}
|
||||
>
|
||||
关联账号
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="accounts-empty">暂无用户数据</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{/* 预设方案快速切换(仅管理员 + 全局策略账号) */}
|
||||
|
|
|
|||
|
|
@ -784,6 +784,14 @@ export const api = {
|
|||
}
|
||||
return response.json();
|
||||
},
|
||||
getUsersDetailed: async () => {
|
||||
const response = await fetch(buildUrl('/api/admin/users/detailed'), { headers: withAuthHeaders() });
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: '获取用户详情失败' }));
|
||||
throw new Error(error.detail || '获取用户详情失败');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// 管理员:获取用户关联的账号列表
|
||||
getUserAccounts: async (userId) => {
|
||||
|
|
@ -794,6 +802,29 @@ export const api = {
|
|||
}
|
||||
return response.json();
|
||||
},
|
||||
grantUserAccount: async (userId, accountId, role = 'viewer') => {
|
||||
const response = await fetch(buildUrl(`/api/admin/users/${userId}/accounts/${accountId}`), {
|
||||
method: 'PUT',
|
||||
headers: withAuthHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ role }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: '授权失败' }));
|
||||
throw new Error(error.detail || '授权失败');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
revokeUserAccount: async (userId, accountId) => {
|
||||
const response = await fetch(buildUrl(`/api/admin/users/${userId}/accounts/${accountId}`), {
|
||||
method: 'DELETE',
|
||||
headers: withAuthHeaders(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: '取消授权失败' }));
|
||||
throw new Error(error.detail || '取消授权失败');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// 管理员:创建用户
|
||||
createUser: async (data) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user