This commit is contained in:
薇薇安 2026-01-22 09:28:58 +08:00
parent 352d36e7a5
commit 28bce8f02b
7 changed files with 792 additions and 94 deletions

View File

@ -67,6 +67,87 @@
color: #111; color: #111;
} }
.nav-user-switcher {
position: relative;
}
.nav-user-switch-btn {
padding: 0.45rem 0.7rem;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.25);
background: rgba(255,255,255,0.08);
color: #fff;
cursor: pointer;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.25rem;
outline: none;
transition: background-color 0.2s;
}
.nav-user-switch-btn:hover {
background: rgba(255,255,255,0.15);
}
.nav-user-popover {
position: absolute;
top: calc(100% + 8px);
left: 0;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
min-width: 200px;
max-width: 300px;
z-index: 1000;
overflow: hidden;
}
.popover-header {
padding: 12px 16px;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
font-weight: 600;
font-size: 14px;
color: #333;
}
.popover-list {
max-height: 300px;
overflow-y: auto;
}
.popover-item {
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s;
border-bottom: 1px solid #f0f0f0;
}
.popover-item:hover {
background: #f5f5f5;
}
.popover-item.active {
background: #e3f2fd;
}
.popover-item:last-child {
border-bottom: none;
}
.popover-item-name {
font-weight: 500;
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.popover-item-meta {
font-size: 12px;
color: #666;
}
@media (min-width: 768px) { @media (min-width: 768px) {
.nav-title { .nav-title {
font-size: 1.5rem; font-size: 1.5rem;

View File

@ -7,6 +7,7 @@ import StatsDashboard from './components/StatsDashboard'
import Recommendations from './components/Recommendations' import Recommendations from './components/Recommendations'
import LogMonitor from './components/LogMonitor' import LogMonitor from './components/LogMonitor'
import AccountSelector from './components/AccountSelector' import AccountSelector from './components/AccountSelector'
import GlobalConfig from './components/GlobalConfig'
import Login from './components/Login' import Login from './components/Login'
import { api, clearAuthToken, clearCurrentAccountId, setCurrentAccountId, getCurrentAccountId } from './services/api' import { api, clearAuthToken, clearCurrentAccountId, setCurrentAccountId, getCurrentAccountId } from './services/api'
import './App.css' import './App.css'
@ -88,7 +89,12 @@ function App() {
<Link to="/recommendations">交易推荐</Link> <Link to="/recommendations">交易推荐</Link>
<Link to="/config">配置</Link> <Link to="/config">配置</Link>
<Link to="/trades">交易记录</Link> <Link to="/trades">交易记录</Link>
{isAdmin ? <Link to="/logs">日志监控</Link> : null} {isAdmin ? (
<>
<Link to="/global-config">全局配置</Link>
<Link to="/logs">日志监控</Link>
</>
) : null}
</div> </div>
<div className="nav-user"> <div className="nav-user">
<span className="nav-user-name"> <span className="nav-user-name">
@ -117,6 +123,7 @@ function App() {
<Route path="/config" element={<ConfigPanel currentUser={me} />} /> <Route path="/config" element={<ConfigPanel currentUser={me} />} />
<Route path="/config/guide" element={<ConfigGuide />} /> <Route path="/config/guide" element={<ConfigGuide />} />
<Route path="/trades" element={<TradeList />} /> <Route path="/trades" element={<TradeList />} />
<Route path="/global-config" element={isAdmin ? <GlobalConfig currentUser={me} /> : <div style={{ padding: '24px' }}>无权限</div>} />
<Route path="/logs" element={isAdmin ? <LogMonitor /> : <div style={{ padding: '24px' }}>无权限</div>} /> <Route path="/logs" element={isAdmin ? <LogMonitor /> : <div style={{ padding: '24px' }}>无权限</div>} />
</Routes> </Routes>
</main> </main>

View File

@ -1,35 +1,51 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState, useRef } from 'react'
import { api, getCurrentAccountId, setCurrentAccountId } from '../services/api' import { api, getCurrentAccountId, setCurrentAccountId } from '../services/api'
const VIEWING_USER_ID_KEY = 'ats_viewing_user_id'
const AccountSelector = ({ onChanged, currentUser }) => { const AccountSelector = ({ onChanged, currentUser }) => {
const [accounts, setAccounts] = useState([]) const [accounts, setAccounts] = useState([])
const [accountId, setAccountId] = useState(getCurrentAccountId()) const [accountId, setAccountId] = useState(getCurrentAccountId())
const [users, setUsers] = useState([]) const [users, setUsers] = useState([])
const [selectedUserId, setSelectedUserId] = useState(null) const [viewingUserId, setViewingUserId] = useState(() => {
// localStorageID
const stored = localStorage.getItem(VIEWING_USER_ID_KEY)
if (stored) {
const parsed = parseInt(stored, 10)
if (Number.isFinite(parsed) && parsed > 0) return parsed
}
return parseInt(String(currentUser?.id || ''), 10) || null
})
const [showUserPopover, setShowUserPopover] = useState(false)
const popoverRef = useRef(null)
const isAdmin = (currentUser?.role || '') === 'admin' const isAdmin = (currentUser?.role || '') === 'admin'
//
const effectiveUserId = isAdmin ? viewingUserId : parseInt(String(currentUser?.id || ''), 10)
// //
useEffect(() => { useEffect(() => {
if (isAdmin) { if (isAdmin) {
api.getUsers() api.getUsers()
.then((list) => { .then((list) => {
setUsers(Array.isArray(list) ? list : []) setUsers(Array.isArray(list) ? list : [])
// // viewingUserId
const currentUserId = parseInt(String(currentUser?.id || ''), 10) const currentUserId = parseInt(String(currentUser?.id || ''), 10)
if (currentUserId && list.some((u) => parseInt(String(u?.id || ''), 10) === currentUserId)) { if (viewingUserId && !list.some((u) => parseInt(String(u?.id || ''), 10) === viewingUserId)) {
setSelectedUserId(currentUserId) setViewingUserId(currentUserId)
} else if (list.length > 0) { localStorage.setItem(VIEWING_USER_ID_KEY, String(currentUserId))
setSelectedUserId(parseInt(String(list[0]?.id || ''), 10))
} }
}) })
.catch(() => setUsers([])) .catch(() => setUsers([]))
} }
}, [isAdmin, currentUser?.id]) }, [isAdmin, currentUser?.id, viewingUserId])
// // effectiveUserId
useEffect(() => { useEffect(() => {
if (isAdmin && selectedUserId) { if (effectiveUserId) {
api.getUserAccounts(selectedUserId) if (isAdmin) {
//
api.getUserAccounts(effectiveUserId)
.then((list) => { .then((list) => {
// {account_id, account_name, account_status} {id, name, status} // {account_id, account_name, account_status} {id, name, status}
const accountsList = (Array.isArray(list) ? list : []).map((item) => ({ const accountsList = (Array.isArray(list) ? list : []).map((item) => ({
@ -50,12 +66,8 @@ const AccountSelector = ({ onChanged, currentUser }) => {
} }
}) })
.catch(() => setAccounts([])) .catch(() => setAccounts([]))
} } else {
}, [isAdmin, selectedUserId])
// //
useEffect(() => {
if (!isAdmin) {
const load = () => { const load = () => {
api.getAccounts() api.getAccounts()
.then((list) => setAccounts(Array.isArray(list) ? list : [])) .then((list) => setAccounts(Array.isArray(list) ? list : []))
@ -68,7 +80,21 @@ const AccountSelector = ({ onChanged, currentUser }) => {
window.addEventListener('ats:accounts:updated', onUpdated) window.addEventListener('ats:accounts:updated', onUpdated)
return () => window.removeEventListener('ats:accounts:updated', onUpdated) return () => window.removeEventListener('ats:accounts:updated', onUpdated)
} }
}, [isAdmin]) }
}, [isAdmin, effectiveUserId])
// popover
useEffect(() => {
const handleClickOutside = (event) => {
if (popoverRef.current && !popoverRef.current.contains(event.target)) {
setShowUserPopover(false)
}
}
if (showUserPopover) {
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}
}, [showUserPopover])
useEffect(() => { useEffect(() => {
setCurrentAccountId(accountId) setCurrentAccountId(accountId)
@ -99,29 +125,55 @@ const AccountSelector = ({ onChanged, currentUser }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [optionsKey, accountId]) }, [optionsKey, accountId])
const currentViewingUser = users.find((u) => parseInt(String(u?.id || ''), 10) === viewingUserId)
const handleSwitchUser = (userId) => {
const nextUserId = parseInt(String(userId || ''), 10)
if (Number.isFinite(nextUserId) && nextUserId > 0) {
setViewingUserId(nextUserId)
localStorage.setItem(VIEWING_USER_ID_KEY, String(nextUserId))
setShowUserPopover(false)
//
setAccountId(null)
}
}
return ( return (
<div className="nav-account"> <div className="nav-account">
{isAdmin ? ( {isAdmin ? (
<> <div className="nav-user-switcher" ref={popoverRef} style={{ position: 'relative' }}>
<span className="nav-account-label">用户</span> <button
<select type="button"
className="nav-account-select" className="nav-user-switch-btn"
value={selectedUserId || ''} onClick={() => setShowUserPopover(!showUserPopover)}
onChange={(e) => { title="切换查看的用户视角"
const v = parseInt(e.target.value, 10)
setSelectedUserId(Number.isFinite(v) && v > 0 ? v : null)
//
setAccountId(null)
}}
title="管理员:先选择用户,再查看该用户下的账号"
> >
👤 {currentViewingUser?.username || '选择用户'}
{currentViewingUser?.role === 'admin' ? '(管理员)' : ''}
<span style={{ marginLeft: '4px' }}></span>
</button>
{showUserPopover && (
<div className="nav-user-popover">
<div className="popover-header">切换用户视角</div>
<div className="popover-list">
{users.map((u) => ( {users.map((u) => (
<option key={u.id} value={u.id}> <div
key={u.id}
className={`popover-item ${viewingUserId === u.id ? 'active' : ''}`}
onClick={() => handleSwitchUser(u.id)}
>
<div className="popover-item-name">
{u.username || 'user'} {u.role === 'admin' ? '(管理员)' : ''} {u.username || 'user'} {u.role === 'admin' ? '(管理员)' : ''}
</option> </div>
<div className="popover-item-meta">
{u.status === 'active' ? '启用' : '禁用'}
</div>
</div>
))} ))}
</select> </div>
</> </div>
)}
</div>
) : null} ) : null}
<span className="nav-account-label">账号</span> <span className="nav-account-label">账号</span>
<select <select
@ -131,11 +183,11 @@ const AccountSelector = ({ onChanged, currentUser }) => {
const v = parseInt(e.target.value, 10) const v = parseInt(e.target.value, 10)
setAccountId(Number.isFinite(v) && v > 0 ? v : 1) setAccountId(Number.isFinite(v) && v > 0 ? v : 1)
}} }}
title={isAdmin ? "切换账号后:配置/持仓/交易记录/统计会按账号隔离;推荐仍是全局" : "切换账号后:配置/持仓/交易记录/统计会按账号隔离;推荐仍是全局"} title="切换账号后:配置/持仓/交易记录/统计会按账号隔离;推荐仍是全局"
disabled={isAdmin && !selectedUserId} disabled={isAdmin && !effectiveUserId}
> >
{options.length === 0 ? ( {options.length === 0 ? (
<option value="">{isAdmin && !selectedUserId ? '请先选择用户' : '暂无账号'}</option> <option value="">{isAdmin && !effectiveUserId ? '请先选择用户' : '暂无账号'}</option>
) : ( ) : (
options.map((a) => ( options.map((a) => (
<option key={a.id} value={a.id}> <option key={a.id} value={a.id}>

View File

@ -855,7 +855,7 @@ const ConfigPanel = ({ currentUser }) => {
<div className="account-switch"> <div className="account-switch">
<span className="account-hint">当前账号#{accountId}在顶部导航切换</span> <span className="account-hint">当前账号#{accountId}在顶部导航切换</span>
</div> </div>
{isAdmin ? ( {isAdmin && currentAccountMeta && String(currentAccountMeta?.status || 'active') === 'active' ? (
<div <div
className="system-hint" className="system-hint"
style={{ style={{
@ -1407,7 +1407,7 @@ const ConfigPanel = ({ currentUser }) => {
) : null} ) : null}
{/* 预设方案快速切换(仅管理员 + 全局策略账号:策略核心统一管理) */} {/* 预设方案快速切换(仅管理员 + 全局策略账号:策略核心统一管理) */}
{isAdmin && isGlobalStrategyAccount ? ( {isAdmin && isGlobalStrategyAccount && currentAccountMeta && String(currentAccountMeta?.status || 'active') === 'active' ? (
<div className="preset-section"> <div className="preset-section">
<div className="preset-header"> <div className="preset-header">
<h3>快速切换方案</h3> <h3>快速切换方案</h3>
@ -1638,7 +1638,9 @@ const ConfigPanel = ({ currentUser }) => {
</div> </div>
)} )}
{Object.entries(configCategories).map(([category, label]) => ( {/* 配置项禁用account时不显示 */}
{currentAccountMeta && String(currentAccountMeta?.status || 'active') === 'active' ? (
Object.entries(configCategories).map(([category, label]) => (
<section key={category} className="config-section"> <section key={category} className="config-section">
<h3>{label}</h3> <h3>{label}</h3>
<div className="config-grid"> <div className="config-grid">
@ -1660,7 +1662,12 @@ const ConfigPanel = ({ currentUser }) => {
))} ))}
</div> </div>
</section> </section>
))} ))
) : currentAccountMeta && String(currentAccountMeta?.status || 'active') === 'disabled' ? (
<div className="system-hint" style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
当前账号已禁用无法查看和修改配置
</div>
) : null}
{showSnapshot && ( {showSnapshot && (
<div className="snapshot-modal-overlay" onClick={() => setShowSnapshot(false)} role="presentation"> <div className="snapshot-modal-overlay" onClick={() => setShowSnapshot(false)} role="presentation">

View File

@ -0,0 +1,167 @@
.global-config {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.global-config-header {
margin-bottom: 32px;
}
.global-config-header h2 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.global-config-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.global-section {
margin-bottom: 40px;
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.section-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.form-card {
background: #f5f5f5;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.form-card h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
font-size: 14px;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
.btn-primary {
background: #1976d2;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary:hover:not(:disabled) {
background: #1565c0;
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.table-container {
overflow-x: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.data-table th {
background: #f5f5f5;
font-weight: 600;
font-size: 14px;
}
.data-table td {
font-size: 14px;
}
.data-table select {
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-badge.active {
background: #e8f5e9;
color: #2e7d32;
}
.status-badge.disabled {
background: #ffebee;
color: #c62828;
}
.message {
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 20px;
}
.message.success {
background: #e8f5e9;
color: #2e7d32;
}
.message.error {
background: #ffebee;
color: #c62828;
}

View File

@ -0,0 +1,328 @@
import React, { useState, useEffect } from 'react'
import { api } from '../services/api'
import './GlobalConfig.css'
const GlobalConfig = ({ currentUser }) => {
const [users, setUsers] = useState([])
const [accounts, setAccounts] = useState([])
const [loading, setLoading] = useState(true)
const [message, setMessage] = useState('')
const [busy, setBusy] = useState(false)
const [selectedUserId, setSelectedUserId] = useState(null)
const [showUserForm, setShowUserForm] = useState(false)
const [newUser, setNewUser] = useState({ username: '', password: '', role: 'user', status: 'active' })
const [editingUserId, setEditingUserId] = useState(null)
useEffect(() => {
loadUsers()
loadAccounts()
}, [])
const loadUsers = async () => {
try {
const list = await api.getUsers()
setUsers(Array.isArray(list) ? list : [])
} catch (error) {
setMessage('加载用户列表失败: ' + (error.message || '未知错误'))
} finally {
setLoading(false)
}
}
const loadAccounts = async () => {
try {
const list = await api.getAccounts()
setAccounts(Array.isArray(list) ? list : [])
} catch (error) {
console.error('加载账号列表失败:', error)
}
}
const handleCreateUser = async () => {
if (!newUser.username || !newUser.password) {
setMessage('用户名和密码不能为空')
return
}
setBusy(true)
setMessage('')
try {
await api.createUser(newUser)
setMessage('用户创建成功')
setShowUserForm(false)
setNewUser({ username: '', password: '', role: 'user', status: 'active' })
await loadUsers()
} catch (error) {
setMessage('创建用户失败: ' + (error.message || '未知错误'))
} finally {
setBusy(false)
}
}
const handleUpdateUserPassword = async (userId) => {
const passwordInput = document.querySelector(`input[data-user-id="${userId}"]`)
const password = passwordInput?.value
if (!password) {
setMessage('密码不能为空')
return
}
setBusy(true)
setMessage('')
try {
await api.updateUserPassword(userId, password)
setMessage('密码更新成功')
setEditingUserId(null)
if (passwordInput) passwordInput.value = ''
await loadUsers()
} catch (error) {
setMessage('更新密码失败: ' + (error.message || '未知错误'))
} finally {
setBusy(false)
}
}
const handleUpdateUserRole = async (userId, role) => {
setBusy(true)
setMessage('')
try {
await api.updateUserRole(userId, role)
setMessage('角色更新成功')
await loadUsers()
} catch (error) {
setMessage('更新角色失败: ' + (error.message || '未知错误'))
} finally {
setBusy(false)
}
}
const handleUpdateUserStatus = async (userId, status) => {
setBusy(true)
setMessage('')
try {
await api.updateUserStatus(userId, status)
setMessage('状态更新成功')
await loadUsers()
} catch (error) {
setMessage('更新状态失败: ' + (error.message || '未知错误'))
} finally {
setBusy(false)
}
}
if (loading) {
return <div className="global-config">加载中...</div>
}
return (
<div className="global-config">
<div className="global-config-header">
<h2>全局配置</h2>
<p>管理用户账号和全局策略配置</p>
</div>
{message && (
<div className={`message ${message.includes('失败') ? 'error' : 'success'}`}>
{message}
</div>
)}
{/* 用户管理 */}
<section className="global-section">
<div className="section-header">
<h3>用户管理</h3>
<button
type="button"
className="btn-primary"
onClick={() => setShowUserForm(!showUserForm)}
disabled={busy}
>
{showUserForm ? '取消' : '+ 创建用户'}
</button>
</div>
{showUserForm && (
<div className="form-card">
<h4>创建新用户</h4>
<div className="form-group">
<label>用户名</label>
<input
type="text"
value={newUser.username}
onChange={(e) => setNewUser({ ...newUser, username: e.target.value })}
placeholder="输入用户名"
/>
</div>
<div className="form-group">
<label>密码</label>
<input
type="password"
value={newUser.password}
onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
placeholder="输入密码"
/>
</div>
<div className="form-group">
<label>角色</label>
<select
value={newUser.role}
onChange={(e) => setNewUser({ ...newUser, role: e.target.value })}
>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
<div className="form-group">
<label>状态</label>
<select
value={newUser.status}
onChange={(e) => setNewUser({ ...newUser, status: e.target.value })}
>
<option value="active">启用</option>
<option value="disabled">禁用</option>
</select>
</div>
<div className="form-actions">
<button type="button" className="btn-primary" onClick={handleCreateUser} disabled={busy}>
创建
</button>
<button type="button" onClick={() => setShowUserForm(false)} disabled={busy}>
取消
</button>
</div>
</div>
)}
<div className="table-container">
<table className="data-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>角色</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.username}</td>
<td>
<select
value={user.role || 'user'}
onChange={(e) => handleUpdateUserRole(user.id, e.target.value)}
disabled={busy}
>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</td>
<td>
<select
value={user.status || 'active'}
onChange={(e) => handleUpdateUserStatus(user.id, e.target.value)}
disabled={busy}
>
<option value="active">启用</option>
<option value="disabled">禁用</option>
</select>
</td>
<td>
{editingUserId === user.id ? (
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<input
type="password"
data-user-id={user.id}
placeholder="新密码"
style={{ padding: '4px 8px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '14px' }}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleUpdateUserPassword(user.id)
}
}}
/>
<button
type="button"
className="btn-primary"
onClick={() => handleUpdateUserPassword(user.id)}
disabled={busy}
>
保存
</button>
<button
type="button"
onClick={() => {
setEditingUserId(null)
const input = document.querySelector(`input[data-user-id="${user.id}"]`)
if (input) input.value = ''
}}
disabled={busy}
>
取消
</button>
</div>
) : (
<button
type="button"
onClick={() => setEditingUserId(user.id)}
disabled={busy}
>
修改密码
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* 账号管理 */}
<section className="global-section">
<div className="section-header">
<h3>账号管理</h3>
<button
type="button"
className="btn-primary"
onClick={loadAccounts}
disabled={busy}
>
刷新
</button>
</div>
<div className="table-container">
<table className="data-table">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>状态</th>
<th>测试网</th>
<th>API Key</th>
</tr>
</thead>
<tbody>
{accounts.map((account) => (
<tr key={account.id}>
<td>{account.id}</td>
<td>{account.name || '未命名'}</td>
<td>
<span className={`status-badge ${account.status === 'active' ? 'active' : 'disabled'}`}>
{account.status === 'active' ? '启用' : '禁用'}
</span>
</td>
<td>{account.use_testnet ? '是' : '否'}</td>
<td>{account.has_api_key ? '已配置' : '未配置'}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
)
}
export default GlobalConfig

View File

@ -630,4 +630,60 @@ export const api = {
} }
return response.json(); return response.json();
}, },
// 管理员:创建用户
createUser: async (data) => {
const response = await fetch(buildUrl('/api/admin/users'), {
method: 'POST',
headers: withAuthHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(data),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '创建用户失败' }));
throw new Error(error.detail || '创建用户失败');
}
return response.json();
},
// 管理员:更新用户密码
updateUserPassword: async (userId, password) => {
const response = await fetch(buildUrl(`/api/admin/users/${userId}/password`), {
method: 'PUT',
headers: withAuthHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ password }),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '更新密码失败' }));
throw new Error(error.detail || '更新密码失败');
}
return response.json();
},
// 管理员:更新用户角色
updateUserRole: async (userId, role) => {
const response = await fetch(buildUrl(`/api/admin/users/${userId}/role`), {
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();
},
// 管理员:更新用户状态
updateUserStatus: async (userId, status) => {
const response = await fetch(buildUrl(`/api/admin/users/${userId}/status`), {
method: 'PUT',
headers: withAuthHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ status }),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '更新状态失败' }));
throw new Error(error.detail || '更新状态失败');
}
return response.json();
},
}; };