This commit is contained in:
薇薇安 2026-02-03 15:00:17 +08:00
parent 614b28493b
commit 50c933a8b0
2 changed files with 177 additions and 0 deletions

View File

@ -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>
)}
{/* 预设方案快速切换(仅管理员 + 全局策略账号) */}

View File

@ -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) => {