update:1.更新用户管理-支持管理员新增用户
This commit is contained in:
@@ -341,7 +341,7 @@ MuMuAINovel/
|
|||||||
## 📧 联系方式
|
## 📧 联系方式
|
||||||
|
|
||||||
- 提交 [Issue](https://github.com/xiamuceer-j/MuMuAINovel/issues)
|
- 提交 [Issue](https://github.com/xiamuceer-j/MuMuAINovel/issues)
|
||||||
- Linux DO [讨论](https://linux.do/t/topic/1100112)
|
- Linux DO [讨论](https://linux.do/t/topic/1106333)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,393 @@
|
|||||||
|
"""
|
||||||
|
管理员API - 用户管理功能
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, HTTPException, Request, Depends
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
import hashlib
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from app.database import get_db, init_db
|
||||||
|
from app.models.user import User
|
||||||
|
from app.user_manager import user_manager
|
||||||
|
from app.user_password import password_manager
|
||||||
|
from app.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/admin", tags=["管理员"])
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 请求/响应模型 ====================
|
||||||
|
|
||||||
|
class CreateUserRequest(BaseModel):
|
||||||
|
"""创建用户请求"""
|
||||||
|
username: str = Field(..., min_length=3, max_length=20, description="用户名")
|
||||||
|
display_name: str = Field(..., min_length=2, max_length=50, description="显示名称")
|
||||||
|
password: Optional[str] = Field(None, min_length=6, description="初始密码,留空则自动生成")
|
||||||
|
avatar_url: Optional[str] = Field(None, description="头像URL")
|
||||||
|
trust_level: int = Field(0, ge=0, le=9, description="信任等级")
|
||||||
|
is_admin: bool = Field(False, description="是否为管理员")
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateUserRequest(BaseModel):
|
||||||
|
"""更新用户请求"""
|
||||||
|
display_name: Optional[str] = Field(None, min_length=2, max_length=50)
|
||||||
|
avatar_url: Optional[str] = None
|
||||||
|
trust_level: Optional[int] = Field(None, ge=-1, le=9)
|
||||||
|
is_admin: Optional[bool] = Field(None, description="是否为管理员")
|
||||||
|
|
||||||
|
|
||||||
|
class ToggleStatusRequest(BaseModel):
|
||||||
|
"""切换用户状态请求"""
|
||||||
|
is_active: bool = Field(..., description="true=启用, false=禁用")
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordRequest(BaseModel):
|
||||||
|
"""重置密码请求"""
|
||||||
|
new_password: Optional[str] = Field(None, min_length=6, description="新密码,留空则重置为默认密码")
|
||||||
|
|
||||||
|
|
||||||
|
class UserResponse(BaseModel):
|
||||||
|
"""用户响应"""
|
||||||
|
user_id: str
|
||||||
|
username: str
|
||||||
|
display_name: str
|
||||||
|
avatar_url: Optional[str]
|
||||||
|
trust_level: int
|
||||||
|
is_admin: bool
|
||||||
|
is_active: bool
|
||||||
|
linuxdo_id: str
|
||||||
|
created_at: str
|
||||||
|
last_login: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateUserResponse(BaseModel):
|
||||||
|
"""创建用户响应"""
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
|
user: dict
|
||||||
|
default_password: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 权限检查依赖 ====================
|
||||||
|
|
||||||
|
async def check_admin(request: Request) -> User:
|
||||||
|
"""检查管理员权限"""
|
||||||
|
user = getattr(request.state, "user", None)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=401, detail="未登录")
|
||||||
|
|
||||||
|
if not user.is_admin:
|
||||||
|
raise HTTPException(status_code=403, detail="需要管理员权限")
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== API 端点 ====================
|
||||||
|
|
||||||
|
@router.get("/users")
|
||||||
|
async def get_users(
|
||||||
|
admin: User = Depends(check_admin),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取用户列表(仅管理员)"""
|
||||||
|
try:
|
||||||
|
all_users = await user_manager.get_all_users()
|
||||||
|
|
||||||
|
users_data = []
|
||||||
|
for user in all_users:
|
||||||
|
# user_manager 返回的是 Pydantic User 对象,直接转为 dict
|
||||||
|
user_dict = user.model_dump()
|
||||||
|
user_dict["is_active"] = user.trust_level != -1
|
||||||
|
users_data.append(user_dict)
|
||||||
|
|
||||||
|
logger.info(f"管理员 {admin.user_id} 获取用户列表,共 {len(users_data)} 个用户")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": len(users_data),
|
||||||
|
"users": users_data
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取用户列表失败: {str(e)}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"获取用户列表失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/users")
|
||||||
|
async def create_user(
|
||||||
|
data: CreateUserRequest,
|
||||||
|
admin: User = Depends(check_admin),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""添加用户(仅管理员)"""
|
||||||
|
try:
|
||||||
|
# 检查用户名是否已存在
|
||||||
|
all_users = await user_manager.get_all_users()
|
||||||
|
for user in all_users:
|
||||||
|
if user.username == data.username:
|
||||||
|
raise HTTPException(status_code=409, detail="用户名已存在")
|
||||||
|
|
||||||
|
# 生成用户ID
|
||||||
|
user_id = f"admin_created_{hashlib.md5(data.username.encode()).hexdigest()[:16]}"
|
||||||
|
|
||||||
|
# 创建用户
|
||||||
|
new_user = await user_manager.create_or_update_from_linuxdo(
|
||||||
|
linuxdo_id=user_id,
|
||||||
|
username=data.username,
|
||||||
|
display_name=data.display_name,
|
||||||
|
avatar_url=data.avatar_url,
|
||||||
|
trust_level=data.trust_level
|
||||||
|
)
|
||||||
|
|
||||||
|
# 设置管理员标志
|
||||||
|
if data.is_admin:
|
||||||
|
# 直接更新数据库中的is_admin字段
|
||||||
|
async with await user_manager._get_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.user_id == user_id)
|
||||||
|
)
|
||||||
|
db_user = result.scalar_one_or_none()
|
||||||
|
if db_user:
|
||||||
|
db_user.is_admin = True
|
||||||
|
await session.commit()
|
||||||
|
new_user.is_admin = True
|
||||||
|
|
||||||
|
# 设置密码
|
||||||
|
actual_password = await password_manager.set_password(
|
||||||
|
user_id=new_user.user_id,
|
||||||
|
username=data.username,
|
||||||
|
password=data.password
|
||||||
|
)
|
||||||
|
|
||||||
|
# 初始化用户数据库
|
||||||
|
try:
|
||||||
|
await init_db(new_user.user_id)
|
||||||
|
logger.info(f"用户 {new_user.user_id} 数据库初始化成功")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"用户 {new_user.user_id} 数据库初始化失败: {e}")
|
||||||
|
|
||||||
|
logger.info(f"管理员 {admin.user_id} 创建了新用户 {new_user.user_id} ({data.username})")
|
||||||
|
|
||||||
|
return CreateUserResponse(
|
||||||
|
success=True,
|
||||||
|
message="用户创建成功",
|
||||||
|
user=new_user.model_dump(),
|
||||||
|
default_password=actual_password if not data.password else None
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建用户失败: {str(e)}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"创建用户失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/users/{user_id}")
|
||||||
|
async def update_user(
|
||||||
|
user_id: str,
|
||||||
|
data: UpdateUserRequest,
|
||||||
|
admin: User = Depends(check_admin),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""编辑用户信息(仅管理员)"""
|
||||||
|
try:
|
||||||
|
# 获取目标用户
|
||||||
|
target_user = await user_manager.get_user(user_id)
|
||||||
|
if not target_user:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
|
||||||
|
# 更新用户信息
|
||||||
|
async with await user_manager._get_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.user_id == user_id)
|
||||||
|
)
|
||||||
|
db_user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not db_user:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
|
||||||
|
# 更新字段
|
||||||
|
if data.display_name is not None:
|
||||||
|
db_user.display_name = data.display_name
|
||||||
|
if data.avatar_url is not None:
|
||||||
|
db_user.avatar_url = data.avatar_url
|
||||||
|
if data.trust_level is not None:
|
||||||
|
db_user.trust_level = data.trust_level
|
||||||
|
if data.is_admin is not None:
|
||||||
|
# 检查是否是最后一个管理员
|
||||||
|
if db_user.is_admin and not data.is_admin:
|
||||||
|
all_users = await user_manager.get_all_users()
|
||||||
|
admin_count = sum(1 for u in all_users if u.is_admin)
|
||||||
|
if admin_count <= 1:
|
||||||
|
raise HTTPException(status_code=400, detail="不能取消最后一个管理员的权限")
|
||||||
|
db_user.is_admin = data.is_admin
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(db_user)
|
||||||
|
|
||||||
|
logger.info(f"管理员 {admin.user_id} 更新了用户 {user_id} 的信息")
|
||||||
|
|
||||||
|
updated_user = await user_manager.get_user(user_id)
|
||||||
|
user_dict = updated_user.model_dump()
|
||||||
|
user_dict["is_active"] = updated_user.trust_level != -1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "用户信息更新成功",
|
||||||
|
"user": user_dict
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新用户失败: {str(e)}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"更新用户失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/users/{user_id}/toggle-status")
|
||||||
|
async def toggle_user_status(
|
||||||
|
user_id: str,
|
||||||
|
data: ToggleStatusRequest,
|
||||||
|
admin: User = Depends(check_admin),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""切换用户状态(启用/禁用)(仅管理员)"""
|
||||||
|
try:
|
||||||
|
# 不允许禁用自己
|
||||||
|
if user_id == admin.user_id:
|
||||||
|
raise HTTPException(status_code=400, detail="不能禁用自己的账号")
|
||||||
|
|
||||||
|
# 获取目标用户
|
||||||
|
target_user = await user_manager.get_user(user_id)
|
||||||
|
if not target_user:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
|
||||||
|
# 更新状态
|
||||||
|
async with await user_manager._get_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.user_id == user_id)
|
||||||
|
)
|
||||||
|
db_user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not db_user:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
|
||||||
|
if data.is_active:
|
||||||
|
# 启用用户:恢复trust_level为0(或之前的值)
|
||||||
|
db_user.trust_level = 0
|
||||||
|
else:
|
||||||
|
# 禁用用户:设置trust_level为-1
|
||||||
|
db_user.trust_level = -1
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
status_text = "启用" if data.is_active else "禁用"
|
||||||
|
logger.info(f"管理员 {admin.user_id} {status_text}了用户 {user_id}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"用户已{status_text}",
|
||||||
|
"is_active": data.is_active
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"切换用户状态失败: {str(e)}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"切换用户状态失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/users/{user_id}/reset-password")
|
||||||
|
async def reset_password(
|
||||||
|
user_id: str,
|
||||||
|
data: ResetPasswordRequest,
|
||||||
|
admin: User = Depends(check_admin),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""重置用户密码(仅管理员)"""
|
||||||
|
try:
|
||||||
|
# 获取目标用户
|
||||||
|
target_user = await user_manager.get_user(user_id)
|
||||||
|
if not target_user:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
|
||||||
|
# 重置密码
|
||||||
|
actual_password = await password_manager.set_password(
|
||||||
|
user_id=user_id,
|
||||||
|
username=target_user.username,
|
||||||
|
password=data.new_password
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"管理员 {admin.user_id} 重置了用户 {user_id} 的密码")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "密码重置成功",
|
||||||
|
"new_password": actual_password
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"重置密码失败: {str(e)}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"重置密码失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/users/{user_id}")
|
||||||
|
async def delete_user(
|
||||||
|
user_id: str,
|
||||||
|
admin: User = Depends(check_admin),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""删除用户(仅管理员,慎用)"""
|
||||||
|
try:
|
||||||
|
# 不允许删除自己
|
||||||
|
if user_id == admin.user_id:
|
||||||
|
raise HTTPException(status_code=400, detail="不能删除自己的账号")
|
||||||
|
|
||||||
|
# 获取目标用户
|
||||||
|
target_user = await user_manager.get_user(user_id)
|
||||||
|
if not target_user:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
|
||||||
|
# 检查是否是最后一个管理员
|
||||||
|
if target_user.is_admin:
|
||||||
|
all_users = await user_manager.get_all_users()
|
||||||
|
admin_count = sum(1 for u in all_users if u.is_admin)
|
||||||
|
if admin_count <= 1:
|
||||||
|
raise HTTPException(status_code=400, detail="不能删除最后一个管理员账号")
|
||||||
|
|
||||||
|
# 删除用户(包括密码记录)
|
||||||
|
async with await user_manager._get_session() as session:
|
||||||
|
# 删除用户记录
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.user_id == user_id)
|
||||||
|
)
|
||||||
|
db_user = result.scalar_one_or_none()
|
||||||
|
if db_user:
|
||||||
|
await session.delete(db_user)
|
||||||
|
|
||||||
|
# 删除密码记录
|
||||||
|
from app.models.user import UserPassword
|
||||||
|
result = await session.execute(
|
||||||
|
select(UserPassword).where(UserPassword.user_id == user_id)
|
||||||
|
)
|
||||||
|
pwd_record = result.scalar_one_or_none()
|
||||||
|
if pwd_record:
|
||||||
|
await session.delete(pwd_record)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
logger.warning(f"管理员 {admin.user_id} 删除了用户 {user_id}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "用户已删除"
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"删除用户失败: {str(e)}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"删除用户失败: {str(e)}")
|
||||||
+2
-1
@@ -135,12 +135,13 @@ from app.api import (
|
|||||||
projects, outlines, characters, chapters,
|
projects, outlines, characters, chapters,
|
||||||
wizard_stream, relationships, organizations,
|
wizard_stream, relationships, organizations,
|
||||||
auth, users, settings, writing_styles, memories,
|
auth, users, settings, writing_styles, memories,
|
||||||
mcp_plugins
|
mcp_plugins, admin
|
||||||
)
|
)
|
||||||
|
|
||||||
app.include_router(auth.router, prefix="/api")
|
app.include_router(auth.router, prefix="/api")
|
||||||
app.include_router(users.router, prefix="/api")
|
app.include_router(users.router, prefix="/api")
|
||||||
app.include_router(settings.router, prefix="/api")
|
app.include_router(settings.router, prefix="/api")
|
||||||
|
app.include_router(admin.router, prefix="/api")
|
||||||
|
|
||||||
app.include_router(projects.router, prefix="/api")
|
app.include_router(projects.router, prefix="/api")
|
||||||
app.include_router(wizard_stream.router, prefix="/api")
|
app.include_router(wizard_stream.router, prefix="/api")
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
认证中间件 - 从 Cookie 中提取用户信息并注入到 request.state
|
认证中间件 - 从 Cookie 中提取用户信息并注入到 request.state
|
||||||
"""
|
"""
|
||||||
from fastapi import Request
|
from fastapi import Request, HTTPException
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from app.user_manager import user_manager
|
from app.user_manager import user_manager
|
||||||
|
from app.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AuthMiddleware(BaseHTTPMiddleware):
|
class AuthMiddleware(BaseHTTPMiddleware):
|
||||||
@@ -20,6 +23,15 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
if user_id:
|
if user_id:
|
||||||
user = await user_manager.get_user(user_id)
|
user = await user_manager.get_user(user_id)
|
||||||
if user:
|
if user:
|
||||||
|
# 检查用户是否被禁用 (trust_level = -1)
|
||||||
|
if user.trust_level == -1:
|
||||||
|
logger.warning(f"禁用用户尝试访问: {user_id} ({user.username})")
|
||||||
|
# 清除用户状态,视为未登录
|
||||||
|
request.state.user_id = None
|
||||||
|
request.state.user = None
|
||||||
|
request.state.is_admin = False
|
||||||
|
else:
|
||||||
|
# 用户正常,注入状态
|
||||||
request.state.user_id = user_id
|
request.state.user_id = user_id
|
||||||
request.state.user = user
|
request.state.user = user
|
||||||
request.state.is_admin = user.is_admin
|
request.state.is_admin = user.is_admin
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ class MCPTestService:
|
|||||||
# 转换为OpenAI Function Calling格式
|
# 转换为OpenAI Function Calling格式
|
||||||
openai_tools = self._convert_tools_to_openai_format(tools)
|
openai_tools = self._convert_tools_to_openai_format(tools)
|
||||||
|
|
||||||
|
logger.info(f"📋 转换后的OpenAI工具数量: {len(openai_tools)}")
|
||||||
|
logger.debug(f"📋 OpenAI工具列表: {[t['function']['name'] for t in openai_tools]}")
|
||||||
|
|
||||||
# 调用AI选择工具
|
# 调用AI选择工具
|
||||||
prompt = f"""你是MCP插件测试助手,需要测试插件 '{plugin.plugin_name}' 的功能。
|
prompt = f"""你是MCP插件测试助手,需要测试插件 '{plugin.plugin_name}' 的功能。
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import ChapterAnalysis from './pages/ChapterAnalysis';
|
|||||||
import WritingStyles from './pages/WritingStyles';
|
import WritingStyles from './pages/WritingStyles';
|
||||||
import Settings from './pages/Settings';
|
import Settings from './pages/Settings';
|
||||||
import MCPPlugins from './pages/MCPPlugins';
|
import MCPPlugins from './pages/MCPPlugins';
|
||||||
|
import UserManagement from './pages/UserManagement';
|
||||||
// import Polish from './pages/Polish';
|
// import Polish from './pages/Polish';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import AuthCallback from './pages/AuthCallback';
|
import AuthCallback from './pages/AuthCallback';
|
||||||
@@ -38,6 +39,7 @@ function App() {
|
|||||||
<Route path="/wizard" element={<ProtectedRoute><ProjectWizardNew /></ProtectedRoute>} />
|
<Route path="/wizard" element={<ProtectedRoute><ProjectWizardNew /></ProtectedRoute>} />
|
||||||
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
|
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
|
||||||
<Route path="/mcp-plugins" element={<ProtectedRoute><MCPPlugins /></ProtectedRoute>} />
|
<Route path="/mcp-plugins" element={<ProtectedRoute><MCPPlugins /></ProtectedRoute>} />
|
||||||
|
<Route path="/user-management" element={<ProtectedRoute><UserManagement /></ProtectedRoute>} />
|
||||||
<Route path="/chapters/:chapterId/reader" element={<ProtectedRoute><ChapterReader /></ProtectedRoute>} />
|
<Route path="/chapters/:chapterId/reader" element={<ProtectedRoute><ChapterReader /></ProtectedRoute>} />
|
||||||
<Route path="/project/:projectId" element={<ProtectedRoute><ProjectDetail /></ProtectedRoute>}>
|
<Route path="/project/:projectId" element={<ProtectedRoute><ProjectDetail /></ProtectedRoute>}>
|
||||||
<Route index element={<Navigate to="world-setting" replace />} />
|
<Route index element={<Navigate to="world-setting" replace />} />
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Dropdown, Avatar, Space, Typography, message, Modal, Table, Button, Tag, Popconfirm, Pagination, Form, Input } from 'antd';
|
import { Dropdown, Avatar, Space, Typography, message, Modal, Form, Input, Button } from 'antd';
|
||||||
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined, KeyOutlined } from '@ant-design/icons';
|
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined } from '@ant-design/icons';
|
||||||
import { authApi, userApi } from '../services/api';
|
import { authApi } from '../services/api';
|
||||||
import type { User } from '../types';
|
import type { User } from '../types';
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
export default function UserMenu() {
|
export default function UserMenu() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||||
const [showUserManagement, setShowUserManagement] = useState(false);
|
|
||||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [pageSize, setPageSize] = useState(10);
|
|
||||||
const [changePasswordForm] = Form.useForm();
|
const [changePasswordForm] = Form.useForm();
|
||||||
const [changingPassword, setChangingPassword] = useState(false);
|
const [changingPassword, setChangingPassword] = useState(false);
|
||||||
|
|
||||||
@@ -42,98 +39,12 @@ export default function UserMenu() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleShowUserManagement = async () => {
|
const handleShowUserManagement = () => {
|
||||||
if (!currentUser?.is_admin) {
|
if (!currentUser?.is_admin) {
|
||||||
message.warning('只有管理员可以访问用户管理');
|
message.warning('只有管理员可以访问用户管理');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
navigate('/user-management');
|
||||||
setShowUserManagement(true);
|
|
||||||
loadUsers();
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadUsers = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const userList = await userApi.listUsers();
|
|
||||||
setUsers(userList);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取用户列表失败:', error);
|
|
||||||
message.error('获取用户列表失败');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetAdmin = async (userId: string, isAdmin: boolean) => {
|
|
||||||
try {
|
|
||||||
await userApi.setAdmin(userId, isAdmin);
|
|
||||||
message.success(isAdmin ? '已设置为管理员' : '已取消管理员权限');
|
|
||||||
loadUsers();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('设置管理员失败:', error);
|
|
||||||
message.error('设置管理员失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteUser = async (userId: string) => {
|
|
||||||
try {
|
|
||||||
await userApi.deleteUser(userId);
|
|
||||||
message.success('用户已删除');
|
|
||||||
loadUsers();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除用户失败:', error);
|
|
||||||
message.error('删除用户失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetPassword = async (userId: string, username: string) => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: '重置用户密码',
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<p>确定要重置用户 <strong>{username}</strong> 的密码吗?</p>
|
|
||||||
<p style={{ color: '#8c8c8c', fontSize: 12 }}>密码将被重置为默认密码:<strong>{username}@666</strong></p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
okText: '确定重置',
|
|
||||||
cancelText: '取消',
|
|
||||||
onOk: async () => {
|
|
||||||
try {
|
|
||||||
const result = await userApi.resetPassword(userId);
|
|
||||||
if (result.default_password) {
|
|
||||||
Modal.success({
|
|
||||||
title: '密码重置成功',
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<p>用户 <strong>{result.username}</strong> 的密码已重置为:</p>
|
|
||||||
<p style={{
|
|
||||||
padding: '8px 12px',
|
|
||||||
background: '#f5f5f5',
|
|
||||||
borderRadius: 4,
|
|
||||||
fontSize: 16,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
color: '#1890ff',
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}}>
|
|
||||||
{result.default_password}
|
|
||||||
</p>
|
|
||||||
<p style={{ color: '#ff4d4f', fontSize: 12, marginTop: 8 }}>
|
|
||||||
请将此密码告知用户,建议用户登录后立即修改密码
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
message.success('密码重置成功');
|
|
||||||
}
|
|
||||||
loadUsers();
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('重置密码失败:', error);
|
|
||||||
message.error(error.response?.data?.detail || '重置密码失败');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangePassword = async (values: { oldPassword: string; newPassword: string }) => {
|
const handleChangePassword = async (values: { oldPassword: string; newPassword: string }) => {
|
||||||
@@ -169,14 +80,17 @@ export default function UserMenu() {
|
|||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
},
|
},
|
||||||
...(currentUser?.is_admin ? [{
|
...(currentUser?.is_admin ? [
|
||||||
|
{
|
||||||
key: 'user-management',
|
key: 'user-management',
|
||||||
icon: <TeamOutlined />,
|
icon: <TeamOutlined />,
|
||||||
label: '用户管理',
|
label: '用户管理',
|
||||||
onClick: handleShowUserManagement,
|
onClick: handleShowUserManagement,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
type: 'divider' as const,
|
type: 'divider' as const,
|
||||||
}] : []),
|
}
|
||||||
|
] : []),
|
||||||
{
|
{
|
||||||
key: 'change-password',
|
key: 'change-password',
|
||||||
icon: <LockOutlined />,
|
icon: <LockOutlined />,
|
||||||
@@ -194,95 +108,6 @@ export default function UserMenu() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: '用户名',
|
|
||||||
dataIndex: 'username',
|
|
||||||
key: 'username',
|
|
||||||
render: (text: string, record: User) => (
|
|
||||||
<Space>
|
|
||||||
<Avatar src={record.avatar_url} icon={<UserOutlined />} size="small" />
|
|
||||||
<div>
|
|
||||||
<div>{record.display_name || text}</div>
|
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>{text}</Text>
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Trust Level',
|
|
||||||
dataIndex: 'trust_level',
|
|
||||||
key: 'trust_level',
|
|
||||||
width: 120,
|
|
||||||
render: (level: number) => <Tag color="blue">{level}</Tag>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '角色',
|
|
||||||
dataIndex: 'is_admin',
|
|
||||||
key: 'is_admin',
|
|
||||||
width: 100,
|
|
||||||
render: (isAdmin: boolean) => (
|
|
||||||
isAdmin ? <Tag color="gold" icon={<CrownOutlined />}>管理员</Tag> : <Tag>普通用户</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '最后登录',
|
|
||||||
dataIndex: 'last_login',
|
|
||||||
key: 'last_login',
|
|
||||||
width: 180,
|
|
||||||
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'actions',
|
|
||||||
width: 280,
|
|
||||||
render: (_: unknown, record: User) => {
|
|
||||||
const isSelf = record.user_id === currentUser?.user_id;
|
|
||||||
return (
|
|
||||||
<Space size="small">
|
|
||||||
{record.is_admin ? (
|
|
||||||
<Popconfirm
|
|
||||||
title="确定要取消管理员权限吗?"
|
|
||||||
onConfirm={() => handleSetAdmin(record.user_id, false)}
|
|
||||||
disabled={isSelf}
|
|
||||||
>
|
|
||||||
<Button size="small" disabled={isSelf}>
|
|
||||||
取消管理员
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => handleSetAdmin(record.user_id, true)}
|
|
||||||
>
|
|
||||||
设为管理员
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<KeyOutlined />}
|
|
||||||
onClick={() => handleResetPassword(record.user_id, record.username)}
|
|
||||||
disabled={isSelf}
|
|
||||||
title={isSelf ? '不能重置自己的密码' : '重置为默认密码'}
|
|
||||||
>
|
|
||||||
重置密码
|
|
||||||
</Button>
|
|
||||||
<Popconfirm
|
|
||||||
title="确定要删除该用户吗?此操作不可恢复!"
|
|
||||||
onConfirm={() => handleDeleteUser(record.user_id)}
|
|
||||||
disabled={isSelf}
|
|
||||||
>
|
|
||||||
<Button size="small" danger disabled={isSelf}>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -365,68 +190,6 @@ export default function UserMenu() {
|
|||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="用户管理"
|
|
||||||
open={showUserManagement}
|
|
||||||
onCancel={() => setShowUserManagement(false)}
|
|
||||||
footer={null}
|
|
||||||
width={900}
|
|
||||||
centered
|
|
||||||
styles={{
|
|
||||||
body: {
|
|
||||||
padding: 0,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
height: 'calc(100vh - 380px)',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
height: '100%',
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
flex: 1,
|
|
||||||
overflow: 'hidden',
|
|
||||||
padding: '0 12px',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
dataSource={users.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
|
|
||||||
rowKey="user_id"
|
|
||||||
loading={loading}
|
|
||||||
pagination={false}
|
|
||||||
scroll={{ x: 800, y: 'calc(100vh - 520px)' }}
|
|
||||||
sticky
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{
|
|
||||||
padding: '16px 24px',
|
|
||||||
borderTop: '1px solid #f0f0f0',
|
|
||||||
background: '#fff',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}>
|
|
||||||
<Pagination
|
|
||||||
current={currentPage}
|
|
||||||
pageSize={pageSize}
|
|
||||||
total={users.length}
|
|
||||||
showSizeChanger
|
|
||||||
showTotal={(total) => `共 ${total} 个用户`}
|
|
||||||
pageSizeOptions={['10', '20', '50', '100']}
|
|
||||||
onChange={(page, newPageSize) => {
|
|
||||||
setCurrentPage(page);
|
|
||||||
setPageSize(newPageSize);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="修改密码"
|
title="修改密码"
|
||||||
open={showChangePassword}
|
open={showChangePassword}
|
||||||
|
|||||||
@@ -554,6 +554,12 @@ export default function MCPPluginsPage() {
|
|||||||
checked={plugin.enabled}
|
checked={plugin.enabled}
|
||||||
onChange={(checked) => handleToggle(plugin, checked)}
|
onChange={(checked) => handleToggle(plugin, checked)}
|
||||||
size={isMobile ? 'small' : 'default'}
|
size={isMobile ? 'small' : 'default'}
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
height: isMobile ? 16 : 22,
|
||||||
|
minHeight: isMobile ? 16 : 22,
|
||||||
|
lineHeight: isMobile ? '16px' : '22px'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="测试连接">
|
<Tooltip title="测试连接">
|
||||||
|
|||||||
@@ -0,0 +1,767 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Switch,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Popconfirm,
|
||||||
|
message,
|
||||||
|
Card,
|
||||||
|
Typography,
|
||||||
|
Badge,
|
||||||
|
InputNumber,
|
||||||
|
Tooltip,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Pagination,
|
||||||
|
Dropdown,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
KeyOutlined,
|
||||||
|
StopOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
MoreOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { adminApi } from '../services/api';
|
||||||
|
import type { User } from '../types';
|
||||||
|
import UserMenu from '../components/UserMenu';
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
interface UserWithStatus extends User {
|
||||||
|
is_active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserManagement() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [users, setUsers] = useState<UserWithStatus[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
|
const [resetPasswordModalVisible, setResetPasswordModalVisible] = useState(false);
|
||||||
|
const [currentUser, setCurrentUser] = useState<UserWithStatus | null>(null);
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [pageSize, setPageSize] = useState(20);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [editForm] = Form.useForm();
|
||||||
|
|
||||||
|
// 过滤用户列表
|
||||||
|
const filteredUsers = users.filter(user => {
|
||||||
|
if (!searchText) return true;
|
||||||
|
const searchLower = searchText.toLowerCase();
|
||||||
|
return (
|
||||||
|
user.username?.toLowerCase().includes(searchLower) ||
|
||||||
|
user.display_name?.toLowerCase().includes(searchLower) ||
|
||||||
|
user.user_id?.toLowerCase().includes(searchLower)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载用户列表
|
||||||
|
const loadUsers = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await adminApi.getUsers();
|
||||||
|
setUsers(res.users);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载用户列表失败:', error);
|
||||||
|
message.error('加载用户列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 添加用户
|
||||||
|
const handleCreate = async (values: any) => {
|
||||||
|
try {
|
||||||
|
const res = await adminApi.createUser(values);
|
||||||
|
message.success('用户创建成功');
|
||||||
|
|
||||||
|
// 如果有默认密码,显示给管理员
|
||||||
|
if (res.default_password) {
|
||||||
|
Modal.info({
|
||||||
|
title: '用户创建成功',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<p>用户名:<Text strong>{values.username}</Text></p>
|
||||||
|
<p>初始密码:<Text strong copyable>{res.default_password}</Text></p>
|
||||||
|
<p style={{ color: '#ff4d4f', marginTop: 16 }}>
|
||||||
|
⚠️ 请复制密码并告知用户,此密码仅显示一次!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
width: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
loadUsers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建用户失败:', error);
|
||||||
|
message.error('创建用户失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑用户
|
||||||
|
const handleEdit = (user: UserWithStatus) => {
|
||||||
|
setCurrentUser(user);
|
||||||
|
editForm.setFieldsValue({
|
||||||
|
display_name: user.display_name,
|
||||||
|
avatar_url: user.avatar_url,
|
||||||
|
trust_level: user.trust_level,
|
||||||
|
is_admin: user.is_admin,
|
||||||
|
});
|
||||||
|
setEditModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async (values: any) => {
|
||||||
|
if (!currentUser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await adminApi.updateUser(currentUser.user_id, values);
|
||||||
|
message.success('用户信息更新成功');
|
||||||
|
setEditModalVisible(false);
|
||||||
|
editForm.resetFields();
|
||||||
|
loadUsers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新用户失败:', error);
|
||||||
|
message.error('更新用户失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换用户状态
|
||||||
|
const handleToggleStatus = async (user: UserWithStatus) => {
|
||||||
|
const isActive = user.is_active !== false;
|
||||||
|
const action = isActive ? '禁用' : '启用';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await adminApi.toggleUserStatus(user.user_id, !isActive);
|
||||||
|
message.success(`用户已${action}`);
|
||||||
|
loadUsers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${action}用户失败:`, error);
|
||||||
|
message.error(`${action}用户失败`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
const handleResetPassword = (user: UserWithStatus) => {
|
||||||
|
setCurrentUser(user);
|
||||||
|
setNewPassword('');
|
||||||
|
setResetPasswordModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetPasswordConfirm = async () => {
|
||||||
|
if (!currentUser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await adminApi.resetPassword(
|
||||||
|
currentUser.user_id,
|
||||||
|
newPassword || undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
Modal.info({
|
||||||
|
title: '密码重置成功',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<p>用户:<Text strong>{currentUser.username}</Text></p>
|
||||||
|
<p>新密码:<Text strong copyable>{res.new_password}</Text></p>
|
||||||
|
<p style={{ color: '#ff4d4f', marginTop: 16 }}>
|
||||||
|
⚠️ 请复制密码并告知用户!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
width: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
setResetPasswordModalVisible(false);
|
||||||
|
setNewPassword('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置密码失败:', error);
|
||||||
|
message.error('重置密码失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
const handleDelete = async (user: UserWithStatus) => {
|
||||||
|
try {
|
||||||
|
await adminApi.deleteUser(user.user_id);
|
||||||
|
message.success('用户已删除');
|
||||||
|
loadUsers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除用户失败:', error);
|
||||||
|
message.error('删除用户失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMobile = window.innerWidth <= 768;
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '用户名',
|
||||||
|
dataIndex: 'username',
|
||||||
|
key: 'username',
|
||||||
|
width: 150,
|
||||||
|
render: (text: string) => (
|
||||||
|
<Space>
|
||||||
|
<UserOutlined style={{ color: '#1890ff' }} />
|
||||||
|
<Text strong>{text}</Text>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '显示名称',
|
||||||
|
dataIndex: 'display_name',
|
||||||
|
key: 'display_name',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'is_active',
|
||||||
|
key: 'is_active',
|
||||||
|
width: 100,
|
||||||
|
render: (isActive: boolean) => (
|
||||||
|
<Badge
|
||||||
|
status={isActive !== false ? 'success' : 'error'}
|
||||||
|
text={isActive !== false ? '正常' : '已禁用'}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '角色',
|
||||||
|
dataIndex: 'is_admin',
|
||||||
|
key: 'is_admin',
|
||||||
|
width: 100,
|
||||||
|
render: (isAdmin: boolean) => (
|
||||||
|
<Tag color={isAdmin ? 'gold' : 'blue'}>
|
||||||
|
{isAdmin ? '👑 管理员' : '普通用户'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '信任等级',
|
||||||
|
dataIndex: 'trust_level',
|
||||||
|
key: 'trust_level',
|
||||||
|
width: 100,
|
||||||
|
render: (level: number) => (
|
||||||
|
<Tag color={level === -1 ? 'default' : level >= 5 ? 'green' : 'blue'}>
|
||||||
|
{level === -1 ? '已禁用' : `Level ${level}`}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
key: 'created_at',
|
||||||
|
width: 180,
|
||||||
|
render: (date: string) => date ? new Date(date).toLocaleString('zh-CN') : '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最后登录',
|
||||||
|
dataIndex: 'last_login',
|
||||||
|
key: 'last_login',
|
||||||
|
width: 180,
|
||||||
|
render: (date: string) => date ? new Date(date).toLocaleString('zh-CN') : '从未登录',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: isMobile ? 80 : 300,
|
||||||
|
fixed: 'right' as const,
|
||||||
|
render: (_: any, record: UserWithStatus) => {
|
||||||
|
const isActive = record.is_active !== false;
|
||||||
|
|
||||||
|
// 移动端:使用下拉菜单
|
||||||
|
if (isMobile) {
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
label: '编辑用户',
|
||||||
|
icon: <EditOutlined />,
|
||||||
|
onClick: () => handleEdit(record),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'reset',
|
||||||
|
label: '重置密码',
|
||||||
|
icon: <KeyOutlined />,
|
||||||
|
onClick: () => handleResetPassword(record),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'toggle',
|
||||||
|
label: isActive ? '禁用用户' : '启用用户',
|
||||||
|
icon: isActive ? <StopOutlined /> : <CheckCircleOutlined />,
|
||||||
|
danger: isActive,
|
||||||
|
onClick: () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确定${isActive ? '禁用' : '启用'}该用户吗?`,
|
||||||
|
onOk: () => handleToggleStatus(record),
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(!record.is_admin ? [{
|
||||||
|
key: 'delete',
|
||||||
|
label: '删除用户',
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
danger: true,
|
||||||
|
onClick: () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确定删除该用户吗?此操作不可恢复!',
|
||||||
|
onOk: () => handleDelete(record),
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
okButtonProps: { danger: true },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}] : []),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown menu={{ items: menuItems }} trigger={['click']}>
|
||||||
|
<Button type="text" icon={<MoreOutlined />} />
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 桌面端:保持原有按钮样式
|
||||||
|
return (
|
||||||
|
<Space size="small">
|
||||||
|
<Tooltip title="编辑用户">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title="重置密码">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
icon={<KeyOutlined />}
|
||||||
|
onClick={() => handleResetPassword(record)}
|
||||||
|
>
|
||||||
|
重置密码
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Popconfirm
|
||||||
|
title={`确定${isActive ? '禁用' : '启用'}该用户吗?`}
|
||||||
|
onConfirm={() => handleToggleStatus(record)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Tooltip title={isActive ? '禁用用户' : '启用用户'}>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
danger={isActive}
|
||||||
|
icon={isActive ? <StopOutlined /> : <CheckCircleOutlined />}
|
||||||
|
>
|
||||||
|
{isActive ? '禁用' : '启用'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
|
||||||
|
{!record.is_admin && (
|
||||||
|
<Popconfirm
|
||||||
|
title="确定删除该用户吗?此操作不可恢复!"
|
||||||
|
onConfirm={() => handleDelete(record)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
>
|
||||||
|
<Tooltip title="删除用户">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
height: '100vh',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
padding: isMobile ? '20px 16px' : '40px 24px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
maxWidth: 1400,
|
||||||
|
margin: '0 auto',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
{/* 顶部导航卡片 */}
|
||||||
|
<Card
|
||||||
|
variant="borderless"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
borderRadius: isMobile ? 12 : 16,
|
||||||
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
||||||
|
marginBottom: isMobile ? 20 : 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Row align="middle" justify="space-between" gutter={[16, 16]}>
|
||||||
|
<Col xs={24} sm={12}>
|
||||||
|
<Space direction="vertical" size={4}>
|
||||||
|
<Title level={isMobile ? 3 : 2} style={{ margin: 0 }}>
|
||||||
|
<TeamOutlined style={{ color: '#fa8c16', marginRight: 8 }} />
|
||||||
|
用户管理
|
||||||
|
</Title>
|
||||||
|
<Text type="secondary" style={{ fontSize: isMobile ? 12 : 14 }}>
|
||||||
|
管理系统用户和权限
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12}>
|
||||||
|
<Space size={12} style={{ display: 'flex', justifyContent: isMobile ? 'flex-start' : 'flex-end', width: '100%' }}>
|
||||||
|
<Button
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
style={{
|
||||||
|
borderRadius: 8,
|
||||||
|
borderColor: '#d9d9d9',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = '#667eea';
|
||||||
|
e.currentTarget.style.color = '#667eea';
|
||||||
|
e.currentTarget.style.boxShadow = '0 2px 12px rgba(102, 126, 234, 0.3)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = '#d9d9d9';
|
||||||
|
e.currentTarget.style.color = 'rgba(0, 0, 0, 0.88)';
|
||||||
|
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
返回主页
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => setModalVisible(true)}
|
||||||
|
style={{
|
||||||
|
borderRadius: 8,
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
border: 'none',
|
||||||
|
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.4)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
添加用户
|
||||||
|
</Button>
|
||||||
|
<UserMenu />
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 主内容卡片 */}
|
||||||
|
<Card
|
||||||
|
variant="borderless"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
borderRadius: isMobile ? 12 : 16,
|
||||||
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
bodyStyle={{
|
||||||
|
padding: 0,
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<div style={{
|
||||||
|
padding: '16px 24px 0 24px',
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
}}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索用户名、显示名称或用户ID"
|
||||||
|
prefix={<SearchOutlined style={{ color: '#999' }} />}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
setCurrentPage(1); // 搜索时重置到第一页
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
style={{
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 表格区域 */}
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: '16px 24px 0 24px',
|
||||||
|
}}>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={filteredUsers.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
|
||||||
|
rowKey="user_id"
|
||||||
|
loading={loading}
|
||||||
|
scroll={{
|
||||||
|
x: 1400,
|
||||||
|
y: 'calc(100vh - 410px)'
|
||||||
|
}}
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 固定分页控件 */}
|
||||||
|
<div style={{
|
||||||
|
padding: '16px 24px 24px 24px',
|
||||||
|
borderTop: '1px solid #f0f0f0',
|
||||||
|
background: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<Pagination
|
||||||
|
current={currentPage}
|
||||||
|
pageSize={pageSize}
|
||||||
|
total={filteredUsers.length}
|
||||||
|
showSizeChanger
|
||||||
|
showTotal={(total) => `共 ${total} 个用户${searchText ? ' (已过滤)' : ''}`}
|
||||||
|
pageSizeOptions={[20, 50, 100]}
|
||||||
|
onChange={(page, size) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
setPageSize(size);
|
||||||
|
}}
|
||||||
|
onShowSizeChange={(_current, size) => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
setPageSize(size);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 添加用户对话框 */}
|
||||||
|
<Modal
|
||||||
|
title={<span><PlusOutlined style={{ marginRight: 8 }} />添加用户</span>}
|
||||||
|
open={modalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
}}
|
||||||
|
onOk={() => form.submit()}
|
||||||
|
width={isMobile ? '90%' : 600}
|
||||||
|
centered
|
||||||
|
okText="创建"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleCreate}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="用户名"
|
||||||
|
name="username"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入用户名' },
|
||||||
|
{ min: 3, max: 20, message: '用户名长度3-20位' },
|
||||||
|
{ pattern: /^[a-zA-Z0-9_]+$/, message: '只能包含字母、数字和下划线' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入用户名" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="显示名称"
|
||||||
|
name="display_name"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入显示名称' },
|
||||||
|
{ min: 2, max: 50, message: '显示名称长度2-50位' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入显示名称" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="初始密码"
|
||||||
|
name="password"
|
||||||
|
extra="留空则自动生成 username@666"
|
||||||
|
rules={[
|
||||||
|
{ min: 6, message: '密码长度至少6位' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="留空则自动生成" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="头像URL"
|
||||||
|
name="avatar_url"
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入头像URL(可选)" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="信任等级"
|
||||||
|
name="trust_level"
|
||||||
|
initialValue={0}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={9} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="设为管理员"
|
||||||
|
name="is_admin"
|
||||||
|
valuePropName="checked"
|
||||||
|
initialValue={false}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
size={isMobile ? 'small' : 'default'}
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
height: isMobile ? 16 : 22,
|
||||||
|
minHeight: isMobile ? 16 : 22,
|
||||||
|
lineHeight: isMobile ? '16px' : '22px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 编辑用户对话框 */}
|
||||||
|
<Modal
|
||||||
|
title={<span><EditOutlined style={{ marginRight: 8 }} />编辑用户</span>}
|
||||||
|
open={editModalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setEditModalVisible(false);
|
||||||
|
editForm.resetFields();
|
||||||
|
}}
|
||||||
|
onOk={() => editForm.submit()}
|
||||||
|
width={isMobile ? '90%' : 600}
|
||||||
|
centered
|
||||||
|
okText="保存"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={editForm}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleUpdate}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="显示名称"
|
||||||
|
name="display_name"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入显示名称' },
|
||||||
|
{ min: 2, max: 50, message: '显示名称长度2-50位' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入显示名称" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="头像URL"
|
||||||
|
name="avatar_url"
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入头像URL(可选)" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="信任等级"
|
||||||
|
name="trust_level"
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={9} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="设为管理员"
|
||||||
|
name="is_admin"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
size={isMobile ? 'small' : 'default'}
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
height: isMobile ? 16 : 22,
|
||||||
|
minHeight: isMobile ? 16 : 22,
|
||||||
|
lineHeight: isMobile ? '16px' : '22px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 重置密码对话框 */}
|
||||||
|
<Modal
|
||||||
|
title={<span><KeyOutlined style={{ marginRight: 8 }} />重置密码</span>}
|
||||||
|
open={resetPasswordModalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setResetPasswordModalVisible(false);
|
||||||
|
setNewPassword('');
|
||||||
|
}}
|
||||||
|
onOk={handleResetPasswordConfirm}
|
||||||
|
width={isMobile ? '90%' : 500}
|
||||||
|
centered
|
||||||
|
okText="确认重置"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Text>用户:<Text strong>{currentUser?.username}</Text></Text>
|
||||||
|
</div>
|
||||||
|
<Form layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
label="新密码"
|
||||||
|
extra="留空则重置为默认密码 username@666"
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
placeholder="留空则使用默认密码"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -521,3 +521,61 @@ export const mcpPluginApi = {
|
|||||||
callTool: (data: MCPToolCallRequest) =>
|
callTool: (data: MCPToolCallRequest) =>
|
||||||
api.post<unknown, MCPToolCallResponse>('/mcp/call', data),
|
api.post<unknown, MCPToolCallResponse>('/mcp/call', data),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 管理员API
|
||||||
|
export const adminApi = {
|
||||||
|
// 获取用户列表
|
||||||
|
getUsers: () =>
|
||||||
|
api.get<unknown, { total: number; users: User[] }>('/admin/users'),
|
||||||
|
|
||||||
|
// 添加用户
|
||||||
|
createUser: (data: {
|
||||||
|
username: string;
|
||||||
|
display_name: string;
|
||||||
|
password?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
trust_level?: number;
|
||||||
|
is_admin?: boolean;
|
||||||
|
}) =>
|
||||||
|
api.post<unknown, {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
user: User;
|
||||||
|
default_password?: string;
|
||||||
|
}>('/admin/users', data),
|
||||||
|
|
||||||
|
// 编辑用户
|
||||||
|
updateUser: (userId: string, data: {
|
||||||
|
display_name?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
trust_level?: number;
|
||||||
|
}) =>
|
||||||
|
api.put<unknown, {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
user: User;
|
||||||
|
}>(`/admin/users/${userId}`, data),
|
||||||
|
|
||||||
|
// 切换用户状态(启用/禁用)
|
||||||
|
toggleUserStatus: (userId: string, isActive: boolean) =>
|
||||||
|
api.post<unknown, {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
is_active: boolean;
|
||||||
|
}>(`/admin/users/${userId}/toggle-status`, { is_active: isActive }),
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
resetPassword: (userId: string, newPassword?: string) =>
|
||||||
|
api.post<unknown, {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
new_password: string;
|
||||||
|
}>(`/admin/users/${userId}/reset-password`, { new_password: newPassword }),
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
deleteUser: (userId: string) =>
|
||||||
|
api.delete<unknown, {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}>(`/admin/users/${userId}`),
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user