feat(account): 添加部门基础管理功能 #480
- 实现数据库迁移,创建部门表并向用户表添加department_id字段。 - 创建Department模型并与User模型建立关联关系。 - 开发部门管理API以支持增删改查操作。 - 添加DepartmentManagementComponent用于在UI中管理部门。 - 在UserManagementComponent中集成部门选择功能以进行用户分配。 - 更新用户存储以处理部门数据。 - 增强UI组件以改善样式和用户体验。
This commit is contained in:
parent
62af84eea9
commit
5b3703ac1c
@ -3,6 +3,7 @@ from fastapi import APIRouter
|
||||
from server.routers.auth_router import auth
|
||||
from server.routers.chat_router import chat
|
||||
from server.routers.dashboard_router import dashboard
|
||||
from server.routers.department_router import department
|
||||
from server.routers.graph_router import graph
|
||||
from server.routers.knowledge_router import knowledge
|
||||
from server.routers.evaluation_router import evaluation
|
||||
@ -18,6 +19,7 @@ router.include_router(system) # /api/system/*
|
||||
router.include_router(auth) # /api/auth/*
|
||||
router.include_router(chat) # /api/chat/*
|
||||
router.include_router(dashboard) # /api/dashboard/*
|
||||
router.include_router(department) # /api/departments/*
|
||||
router.include_router(knowledge) # /api/knowledge/*
|
||||
router.include_router(evaluation) # /api/evaluation/*
|
||||
router.include_router(mindmap) # /api/mindmap/*
|
||||
|
||||
@ -8,7 +8,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.storage.db.manager import db_manager
|
||||
from src.storage.db.models import User
|
||||
from src.storage.db.models import User, Department
|
||||
from server.utils.auth_middleware import get_admin_user, get_current_user, get_db, get_required_user
|
||||
from server.utils.auth_utils import AuthUtils
|
||||
from server.utils.user_utils import generate_unique_user_id, validate_username, is_valid_phone_number
|
||||
@ -37,6 +37,7 @@ class UserCreate(BaseModel):
|
||||
password: str
|
||||
role: str = "user"
|
||||
phone_number: str | None = None
|
||||
department_id: int | None = None
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
@ -45,6 +46,7 @@ class UserUpdate(BaseModel):
|
||||
role: str | None = None
|
||||
phone_number: str | None = None
|
||||
avatar: str | None = None
|
||||
department_id: int | None = None
|
||||
|
||||
|
||||
class UserProfileUpdate(BaseModel):
|
||||
@ -59,6 +61,8 @@ class UserResponse(BaseModel):
|
||||
phone_number: str | None = None
|
||||
avatar: str | None = None
|
||||
role: str
|
||||
department_id: int | None = None
|
||||
department_name: str | None = None # 部门名称
|
||||
created_at: str
|
||||
last_login: str | None = None
|
||||
|
||||
@ -84,6 +88,13 @@ class UserIdGeneration(BaseModel):
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def get_default_department_id(db: AsyncSession) -> int | None:
|
||||
"""获取默认部门的ID"""
|
||||
result = await db.execute(select(Department).filter(Department.name == "默认部门"))
|
||||
default_dept = result.scalar_one_or_none()
|
||||
return default_dept.id if default_dept else None
|
||||
|
||||
|
||||
# 路由:登录获取令牌
|
||||
# =============================================================================
|
||||
# === 认证分组 ===
|
||||
@ -174,6 +185,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(
|
||||
"phone_number": user.phone_number,
|
||||
"avatar": user.avatar,
|
||||
"role": user.role,
|
||||
"department_id": user.department_id,
|
||||
}
|
||||
|
||||
|
||||
@ -217,6 +229,14 @@ async def initialize_admin(admin_data: InitializeAdmin, db: AsyncSession = Depen
|
||||
# 由于是首次初始化,直接使用输入的user_id
|
||||
user_id = admin_data.user_id
|
||||
|
||||
# 创建默认部门
|
||||
default_department = Department(
|
||||
name="默认部门",
|
||||
description="系统初始化时创建的默认部门"
|
||||
)
|
||||
db.add(default_department)
|
||||
await db.flush() # 获取部门ID
|
||||
|
||||
new_admin = User(
|
||||
username=admin_data.user_id, # username和user_id设置为相同值
|
||||
user_id=user_id,
|
||||
@ -224,6 +244,7 @@ async def initialize_admin(admin_data: InitializeAdmin, db: AsyncSession = Depen
|
||||
avatar=None, # 初始化时头像为空
|
||||
password_hash=hashed_password,
|
||||
role="superadmin",
|
||||
department_id=default_department.id,
|
||||
last_login=utc_now(),
|
||||
)
|
||||
|
||||
@ -389,12 +410,29 @@ async def create_user(
|
||||
detail="管理员只能创建普通用户账户",
|
||||
)
|
||||
|
||||
# 部门分配逻辑
|
||||
if current_user.role == "superadmin":
|
||||
# 超级管理员创建用户时,使用指定的部门或默认部门
|
||||
department_id = user_data.department_id
|
||||
if department_id is None:
|
||||
department_id = await get_default_department_id(db)
|
||||
else:
|
||||
# 普通管理员创建用户时,自动继承该管理员的部门
|
||||
department_id = current_user.department_id
|
||||
# 非超级管理员不能指定部门
|
||||
if user_data.department_id is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="普通管理员不能指定部门",
|
||||
)
|
||||
|
||||
new_user = User(
|
||||
username=user_data.username,
|
||||
user_id=user_id,
|
||||
phone_number=user_data.phone_number,
|
||||
password_hash=hashed_password,
|
||||
role=user_data.role,
|
||||
department_id=department_id,
|
||||
)
|
||||
|
||||
db.add(new_user)
|
||||
@ -414,9 +452,35 @@ async def create_user(
|
||||
async def read_users(
|
||||
skip: int = 0, limit: int = 100, current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
result = await db.execute(select(User).filter(User.is_deleted == 0).offset(skip).limit(limit))
|
||||
users = result.scalars().all()
|
||||
return [user.to_dict() for user in users]
|
||||
# 部门隔离逻辑
|
||||
if current_user.role == "superadmin":
|
||||
# 超级管理员可以看到所有用户,使用 JOIN 获取部门名称
|
||||
result = await db.execute(
|
||||
select(User, Department.name.label("department_name"))
|
||||
.outerjoin(Department, User.department_id == Department.id)
|
||||
.filter(User.is_deleted == 0)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
else:
|
||||
# 普通管理员只能看到本部门用户
|
||||
result = await db.execute(
|
||||
select(User, Department.name.label("department_name"))
|
||||
.outerjoin(Department, User.department_id == Department.id)
|
||||
.filter(
|
||||
User.is_deleted == 0,
|
||||
User.department_id == current_user.department_id
|
||||
)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
rows = result.all()
|
||||
users = []
|
||||
for user, dept_name in rows:
|
||||
user_dict = user.to_dict()
|
||||
user_dict["department_name"] = dept_name
|
||||
users.append(user_dict)
|
||||
return users
|
||||
|
||||
|
||||
# 路由:获取特定用户信息(管理员权限)
|
||||
@ -494,6 +558,16 @@ async def update_user(
|
||||
user.avatar = user_data.avatar
|
||||
update_details.append(f"头像: {user_data.avatar or '已清空'}")
|
||||
|
||||
# 部门修改权限控制(只有超级管理员可以修改用户部门)
|
||||
if user_data.department_id is not None:
|
||||
if current_user.role != "superadmin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="只有超级管理员才能修改用户部门",
|
||||
)
|
||||
user.department_id = user_data.department_id
|
||||
update_details.append(f"部门ID: {user_data.department_id}")
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 记录操作
|
||||
|
||||
308
server/routers/department_router.py
Normal file
308
server/routers/department_router.py
Normal file
@ -0,0 +1,308 @@
|
||||
"""
|
||||
部门管理路由
|
||||
提供部门的增删改查接口,仅超级管理员可访问
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.storage.db.models import Department, User
|
||||
from server.utils.auth_middleware import get_superadmin_user, get_db
|
||||
from server.utils.auth_utils import AuthUtils
|
||||
from server.utils.common_utils import log_operation
|
||||
from server.utils.user_utils import validate_username, is_valid_phone_number, generate_unique_user_id
|
||||
from src.utils.datetime_utils import utc_now
|
||||
|
||||
# 创建路由器
|
||||
department = APIRouter(prefix="/departments", tags=["department"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# === 请求和响应模型 ===
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class DepartmentCreate(BaseModel):
|
||||
"""创建部门请求"""
|
||||
name: str
|
||||
description: str | None = None
|
||||
# 必需的管理员信息
|
||||
admin_user_id: str
|
||||
admin_password: str
|
||||
admin_phone: str | None = None
|
||||
|
||||
|
||||
class DepartmentCreateWithoutAdmin(BaseModel):
|
||||
"""创建部门请求(无管理员,用于兼容)"""
|
||||
name: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class DepartmentUpdate(BaseModel):
|
||||
"""更新部门请求"""
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class DepartmentResponse(BaseModel):
|
||||
"""部门响应"""
|
||||
id: int
|
||||
name: str
|
||||
description: str | None = None
|
||||
created_at: str
|
||||
user_count: int = 0
|
||||
|
||||
|
||||
class DepartmentSimpleResponse(BaseModel):
|
||||
"""部门简单响应(不含用户数量)"""
|
||||
id: int
|
||||
name: str
|
||||
description: str | None = None
|
||||
created_at: str
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# === 部门管理路由 ===
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@department.get("", response_model=list[DepartmentResponse])
|
||||
async def get_departments(
|
||||
current_user: User = Depends(get_superadmin_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取所有部门列表"""
|
||||
result = await db.execute(select(Department).order_by(Department.created_at.desc()))
|
||||
departments = result.scalars().all()
|
||||
|
||||
# 获取每个部门的用户数量
|
||||
department_list = []
|
||||
for dep in departments:
|
||||
user_count_result = await db.execute(
|
||||
select(func.count(User.id)).filter(
|
||||
User.department_id == dep.id,
|
||||
User.is_deleted == 0
|
||||
)
|
||||
)
|
||||
user_count = user_count_result.scalar()
|
||||
department_list.append({**dep.to_dict(), "user_count": user_count})
|
||||
|
||||
return department_list
|
||||
|
||||
|
||||
@department.get("/{department_id}", response_model=DepartmentResponse)
|
||||
async def get_department(
|
||||
department_id: int,
|
||||
current_user: User = Depends(get_superadmin_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取指定部门详情"""
|
||||
result = await db.execute(select(Department).filter(Department.id == department_id))
|
||||
department = result.scalar_one_or_none()
|
||||
|
||||
if not department:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="部门不存在"
|
||||
)
|
||||
|
||||
# 获取部门下用户数量
|
||||
user_count_result = await db.execute(
|
||||
select(func.count(User.id)).filter(
|
||||
User.department_id == department_id,
|
||||
User.is_deleted == 0
|
||||
)
|
||||
)
|
||||
user_count = user_count_result.scalar()
|
||||
|
||||
return {
|
||||
**department.to_dict(),
|
||||
"user_count": user_count
|
||||
}
|
||||
|
||||
|
||||
@department.post("", response_model=DepartmentResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_department(
|
||||
department_data: DepartmentCreate,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_superadmin_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""创建新部门,同时创建该部门的管理员"""
|
||||
# 检查部门名称是否已存在
|
||||
result = await db.execute(select(Department).filter(Department.name == department_data.name))
|
||||
existing = result.scalar_one_or_none()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="部门名称已存在"
|
||||
)
|
||||
|
||||
# 验证管理员 user_id 格式
|
||||
admin_user_id = department_data.admin_user_id
|
||||
if not re.match(r"^[a-zA-Z0-9_]+$", admin_user_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="用户ID只能包含字母、数字和下划线",
|
||||
)
|
||||
|
||||
if len(admin_user_id) < 3 or len(admin_user_id) > 20:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="用户ID长度必须在3-20个字符之间",
|
||||
)
|
||||
|
||||
# 检查 user_id 是否已存在
|
||||
result = await db.execute(select(User).filter(User.user_id == admin_user_id))
|
||||
existing_user = result.scalar_one_or_none()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="用户ID已存在",
|
||||
)
|
||||
|
||||
# 检查手机号是否已存在(如果提供了)
|
||||
admin_phone = department_data.admin_phone
|
||||
if admin_phone:
|
||||
if not is_valid_phone_number(admin_phone):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="手机号格式不正确")
|
||||
result = await db.execute(select(User).filter(User.phone_number == admin_phone))
|
||||
existing_phone = result.scalar_one_or_none()
|
||||
if existing_phone:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="手机号已存在",
|
||||
)
|
||||
|
||||
new_department = Department(
|
||||
name=department_data.name,
|
||||
description=department_data.description
|
||||
)
|
||||
|
||||
db.add(new_department)
|
||||
await db.flush() # 获取部门ID
|
||||
|
||||
# 创建管理员用户
|
||||
hashed_password = AuthUtils.hash_password(department_data.admin_password)
|
||||
new_admin = User(
|
||||
username=admin_user_id, # username 和 user_id 设置为相同值
|
||||
user_id=admin_user_id,
|
||||
phone_number=admin_phone,
|
||||
password_hash=hashed_password,
|
||||
role="admin",
|
||||
department_id=new_department.id,
|
||||
)
|
||||
db.add(new_admin)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(new_department)
|
||||
|
||||
# 记录操作
|
||||
await log_operation(
|
||||
db, current_user.id, "创建部门", f"创建部门: {department_data.name},并创建管理员: {admin_user_id}", request
|
||||
)
|
||||
|
||||
return {
|
||||
**new_department.to_dict(),
|
||||
"user_count": 1
|
||||
}
|
||||
|
||||
|
||||
@department.put("/{department_id}", response_model=DepartmentResponse)
|
||||
async def update_department(
|
||||
department_id: int,
|
||||
department_data: DepartmentUpdate,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_superadmin_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""更新部门信息"""
|
||||
result = await db.execute(select(Department).filter(Department.id == department_id))
|
||||
department = result.scalar_one_or_none()
|
||||
|
||||
if not department:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="部门不存在"
|
||||
)
|
||||
|
||||
# 如果要修改名称,检查新名称是否已存在
|
||||
if department_data.name and department_data.name != department.name:
|
||||
result = await db.execute(select(Department).filter(Department.name == department_data.name))
|
||||
existing = result.scalar_one_or_none()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="部门名称已存在"
|
||||
)
|
||||
department.name = department_data.name
|
||||
|
||||
if department_data.description is not None:
|
||||
department.description = department_data.description
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(department)
|
||||
|
||||
# 记录操作
|
||||
await log_operation(db, current_user.id, "更新部门", f"更新部门: {department.name}", request)
|
||||
|
||||
# 获取部门下用户数量
|
||||
user_count_result = await db.execute(
|
||||
select(func.count(User.id)).filter(
|
||||
User.department_id == department_id,
|
||||
User.is_deleted == 0
|
||||
)
|
||||
)
|
||||
user_count = user_count_result.scalar()
|
||||
|
||||
return {
|
||||
**department.to_dict(),
|
||||
"user_count": user_count
|
||||
}
|
||||
|
||||
|
||||
@department.delete("/{department_id}", status_code=status.HTTP_200_OK)
|
||||
async def delete_department(
|
||||
department_id: int,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_superadmin_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""删除部门"""
|
||||
# 检查部门是否存在
|
||||
result = await db.execute(select(Department).filter(Department.id == department_id))
|
||||
department = result.scalar_one_or_none()
|
||||
|
||||
if not department:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="部门不存在"
|
||||
)
|
||||
|
||||
# 检查部门下是否有用户
|
||||
user_count_result = await db.execute(
|
||||
select(func.count(User.id)).filter(
|
||||
User.department_id == department_id,
|
||||
User.is_deleted == 0
|
||||
)
|
||||
)
|
||||
user_count = user_count_result.scalar()
|
||||
|
||||
if user_count > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"无法删除部门,该部门下还有 {user_count} 个用户"
|
||||
)
|
||||
|
||||
department_name = department.name
|
||||
await db.delete(department)
|
||||
await db.commit()
|
||||
|
||||
# 记录操作
|
||||
await log_operation(db, current_user.id, "删除部门", f"删除部门: {department_name}", request)
|
||||
|
||||
return {"success": True, "message": "部门已删除"}
|
||||
@ -156,6 +156,27 @@ class DatabaseMigrator:
|
||||
if "conn" in locals():
|
||||
conn.close()
|
||||
|
||||
def check_table_exists(self, table_name: str) -> bool:
|
||||
"""检查表是否存在"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name=?
|
||||
""",
|
||||
(table_name,),
|
||||
)
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
finally:
|
||||
if "conn" in locals():
|
||||
conn.close()
|
||||
|
||||
def run_migrations(self):
|
||||
"""运行所有待执行的迁移"""
|
||||
current_version = self.get_current_version()
|
||||
@ -266,6 +287,29 @@ class DatabaseMigrator:
|
||||
|
||||
migrations.append((3, "为消息表添加多模态图片支持字段", v3_commands))
|
||||
|
||||
# 迁移 v4: 添加部门功能
|
||||
v4_commands: list[str] = []
|
||||
|
||||
# 检查 departments 表是否存在
|
||||
if not self.check_table_exists("departments"):
|
||||
v4_commands.append("""
|
||||
CREATE TABLE departments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(50) NOT NULL UNIQUE,
|
||||
description VARCHAR(255),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
v4_commands.append("CREATE INDEX idx_departments_name ON departments(name)")
|
||||
|
||||
# 检查 users 表是否有 department_id 字段
|
||||
if not self.check_column_exists("users", "department_id"):
|
||||
v4_commands.append("ALTER TABLE users ADD COLUMN department_id INTEGER REFERENCES departments(id)")
|
||||
|
||||
v4_commands.append("CREATE INDEX idx_users_department_id ON users(department_id)")
|
||||
|
||||
migrations.append((4, "添加部门功能", v4_commands))
|
||||
|
||||
# 未来的迁移可以在这里添加
|
||||
# migrations.append((
|
||||
# 2,
|
||||
|
||||
@ -56,11 +56,11 @@ class Config(BaseModel):
|
||||
)
|
||||
embed_model: str = Field(
|
||||
default="siliconflow/BAAI/bge-m3",
|
||||
description="Embedding 模型",
|
||||
description="默认 Embedding 模型",
|
||||
)
|
||||
reranker: str = Field(
|
||||
default="siliconflow/BAAI/bge-reranker-v2-m3",
|
||||
description="Re-Ranker 模型",
|
||||
description="默认 Re-Ranker 模型",
|
||||
)
|
||||
content_guard_llm_model: str = Field(
|
||||
default="siliconflow/Qwen/Qwen3-235B-A22B-Instruct-2507",
|
||||
|
||||
@ -21,6 +21,28 @@ def _format_utc_datetime(dt_value):
|
||||
## Removed legacy RDBMS knowledge models (KnowledgeDatabase/KnowledgeFile/KnowledgeNode)
|
||||
|
||||
|
||||
class Department(Base):
|
||||
"""部门模型"""
|
||||
|
||||
__tablename__ = "departments"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String(50), nullable=False, unique=True, index=True)
|
||||
description = Column(String(255), nullable=True)
|
||||
created_at = Column(DateTime, default=utc_now)
|
||||
|
||||
# 关联关系
|
||||
users = relationship("User", back_populates="department")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"created_at": _format_utc_datetime(self.created_at),
|
||||
}
|
||||
|
||||
|
||||
class Conversation(Base):
|
||||
"""Conversation table - new storage system"""
|
||||
|
||||
@ -176,6 +198,7 @@ class User(Base):
|
||||
avatar = Column(String, nullable=True) # 头像URL
|
||||
password_hash = Column(String, nullable=False)
|
||||
role = Column(String, nullable=False, default="user") # 角色: superadmin, admin, user
|
||||
department_id = Column(Integer, ForeignKey("departments.id"), nullable=True) # 部门ID
|
||||
created_at = Column(DateTime, default=utc_now)
|
||||
last_login = Column(DateTime, nullable=True)
|
||||
|
||||
@ -191,6 +214,9 @@ class User(Base):
|
||||
# 关联操作日志
|
||||
operation_logs = relationship("OperationLog", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
# 关联部门
|
||||
department = relationship("Department", back_populates="users")
|
||||
|
||||
def to_dict(self, include_password=False):
|
||||
# SQLite 存储 naive datetime,需要标记为 UTC 后再转换
|
||||
result = {
|
||||
@ -200,6 +226,7 @@ class User(Base):
|
||||
"phone_number": self.phone_number,
|
||||
"avatar": self.avatar,
|
||||
"role": self.role,
|
||||
"department_id": self.department_id,
|
||||
"created_at": _format_utc_datetime(self.created_at),
|
||||
"last_login": _format_utc_datetime(self.last_login),
|
||||
"login_failed_count": self.login_failed_count,
|
||||
|
||||
69
web/src/apis/department_api.js
Normal file
69
web/src/apis/department_api.js
Normal file
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 部门管理 API
|
||||
*/
|
||||
|
||||
import {
|
||||
apiSuperAdminGet,
|
||||
apiSuperAdminPost,
|
||||
apiSuperAdminPut,
|
||||
apiSuperAdminDelete
|
||||
} from './base'
|
||||
|
||||
const BASE_URL = '/api/departments'
|
||||
|
||||
/**
|
||||
* 获取部门列表
|
||||
* @returns {Promise<Array>} 部门列表
|
||||
*/
|
||||
export const getDepartments = () => {
|
||||
return apiSuperAdminGet(BASE_URL)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门详情
|
||||
* @param {number} departmentId - 部门ID
|
||||
* @returns {Promise<Object>} 部门详情
|
||||
*/
|
||||
export const getDepartment = (departmentId) => {
|
||||
return apiSuperAdminGet(`${BASE_URL}/${departmentId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建部门
|
||||
* @param {Object} data - 部门数据
|
||||
* @param {string} data.name - 部门名称
|
||||
* @param {string} [data.description] - 部门描述
|
||||
* @returns {Promise<Object>} 创建的部门
|
||||
*/
|
||||
export const createDepartment = (data) => {
|
||||
return apiSuperAdminPost(BASE_URL, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新部门
|
||||
* @param {number} departmentId - 部门ID
|
||||
* @param {Object} data - 部门数据
|
||||
* @param {string} [data.name] - 部门名称
|
||||
* @param {string} [data.description] - 部门描述
|
||||
* @returns {Promise<Object>} 更新后的部门
|
||||
*/
|
||||
export const updateDepartment = (departmentId, data) => {
|
||||
return apiSuperAdminPut(`${BASE_URL}/${departmentId}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除部门
|
||||
* @param {number} departmentId - 部门ID
|
||||
* @returns {Promise<Object>} 删除结果
|
||||
*/
|
||||
export const deleteDepartment = (departmentId) => {
|
||||
return apiSuperAdminDelete(`${BASE_URL}/${departmentId}`)
|
||||
}
|
||||
|
||||
export const departmentApi = {
|
||||
getDepartments,
|
||||
getDepartment,
|
||||
createDepartment,
|
||||
updateDepartment,
|
||||
deleteDepartment
|
||||
}
|
||||
@ -10,6 +10,7 @@ export * from './graph_api' // 图谱API
|
||||
export * from './agent_api' // 智能体API
|
||||
export * from './tasker' // 任务管理API
|
||||
export * from './mindmap_api' // 思维导图API
|
||||
export * from './department_api' // 部门管理API
|
||||
|
||||
// 导出基础工具函数
|
||||
export {
|
||||
|
||||
@ -238,7 +238,7 @@ const openLink = (url) => {
|
||||
padding-bottom: 8px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -337,7 +337,7 @@ const openLink = (url) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 8px;
|
||||
background: var(--gray-0);
|
||||
|
||||
558
web/src/components/DepartmentManagementComponent.vue
Normal file
558
web/src/components/DepartmentManagementComponent.vue
Normal file
@ -0,0 +1,558 @@
|
||||
<template>
|
||||
<div class="department-management">
|
||||
<!-- 头部区域 -->
|
||||
<div class="header-section">
|
||||
<div class="header-content">
|
||||
<h3 class="title">部门管理</h3>
|
||||
<p class="description">管理系统部门,部门下的用户会被隔离管理。</p>
|
||||
</div>
|
||||
<a-button type="primary" @click="showAddDepartmentModal" class="add-btn">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加部门
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="content-section">
|
||||
<a-spin :spinning="departmentManagement.loading">
|
||||
<div v-if="departmentManagement.error" class="error-message">
|
||||
<a-alert type="error" :message="departmentManagement.error" show-icon />
|
||||
</div>
|
||||
|
||||
<template v-if="departmentManagement.departments.length > 0">
|
||||
<a-table
|
||||
:dataSource="departmentManagement.departments"
|
||||
:columns="columns"
|
||||
:rowKey="record => record.id"
|
||||
:pagination="false"
|
||||
class="department-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="department-name">
|
||||
<span class="name-text">{{ record.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'description'">
|
||||
<span class="description-text">{{ record.description || '-' }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'userCount'">
|
||||
<span>{{ record.user_count ?? 0 }} 人</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-tooltip title="编辑部门">
|
||||
<a-button type="text" size="small" @click="showEditDepartmentModal(record)" class="action-btn">
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="删除部门">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
@click="confirmDeleteDepartment(record)"
|
||||
:disabled="record.user_count > 0"
|
||||
class="action-btn"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<a-empty description="暂无部门数据" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<!-- 部门表单模态框 -->
|
||||
<a-modal
|
||||
v-model:open="departmentManagement.modalVisible"
|
||||
:title="departmentManagement.modalTitle"
|
||||
@ok="handleDepartmentFormSubmit"
|
||||
:confirmLoading="departmentManagement.loading"
|
||||
@cancel="departmentManagement.modalVisible = false"
|
||||
:maskClosable="false"
|
||||
width="520px"
|
||||
class="department-modal"
|
||||
>
|
||||
<a-form layout="vertical" class="department-form">
|
||||
<a-form-item label="部门名称" required class="form-item">
|
||||
<a-input
|
||||
v-model:value="departmentManagement.form.name"
|
||||
placeholder="请输入部门名称"
|
||||
size="large"
|
||||
:maxlength="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="部门描述" class="form-item">
|
||||
<a-textarea
|
||||
v-model:value="departmentManagement.form.description"
|
||||
placeholder="请输入部门描述(可选)"
|
||||
:rows="3"
|
||||
:maxlength="255"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider v-if="!departmentManagement.editMode" />
|
||||
|
||||
<template v-if="!departmentManagement.editMode">
|
||||
<div class="admin-section-title">
|
||||
<TeamOutlined />
|
||||
<span>部门管理员</span>
|
||||
</div>
|
||||
<p class="admin-section-hint">创建部门时必须同时创建管理员,该管理员将负责管理本部门用户</p>
|
||||
|
||||
<a-form-item label="管理员用户ID" required class="form-item">
|
||||
<a-input
|
||||
v-model:value="departmentManagement.form.adminUserId"
|
||||
placeholder="请输入管理员用户ID(3-20位字母/数字/下划线)"
|
||||
size="large"
|
||||
:maxlength="20"
|
||||
@blur="checkAdminUserId"
|
||||
/>
|
||||
<div v-if="departmentManagement.form.userIdError" class="error-text">
|
||||
{{ departmentManagement.form.userIdError }}
|
||||
</div>
|
||||
<div v-else class="help-text">此ID将用于登录</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="密码" required class="form-item">
|
||||
<a-input-password
|
||||
v-model:value="departmentManagement.form.adminPassword"
|
||||
placeholder="请输入管理员密码"
|
||||
size="large"
|
||||
:maxlength="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="确认密码" required class="form-item">
|
||||
<a-input-password
|
||||
v-model:value="departmentManagement.form.adminConfirmPassword"
|
||||
placeholder="请再次输入密码"
|
||||
size="large"
|
||||
:maxlength="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="手机号(可选)" class="form-item">
|
||||
<a-input
|
||||
v-model:value="departmentManagement.form.adminPhone"
|
||||
placeholder="请输入手机号(可用于登录)"
|
||||
size="large"
|
||||
:maxlength="11"
|
||||
/>
|
||||
<div v-if="departmentManagement.form.phoneError" class="error-text">
|
||||
{{ departmentManagement.form.phoneError }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, onMounted, watch } from 'vue'
|
||||
import { notification, Modal } from 'ant-design-vue'
|
||||
import { departmentApi, apiSuperAdminGet } from '@/apis'
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, TeamOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '部门名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '用户数量',
|
||||
dataIndex: 'user_count',
|
||||
key: 'userCount',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
// 部门管理状态
|
||||
const departmentManagement = reactive({
|
||||
loading: false,
|
||||
departments: [],
|
||||
error: null,
|
||||
modalVisible: false,
|
||||
modalTitle: '添加部门',
|
||||
editMode: false,
|
||||
editDepartmentId: null,
|
||||
form: {
|
||||
name: '',
|
||||
description: '',
|
||||
adminUserId: '',
|
||||
adminPassword: '',
|
||||
adminConfirmPassword: '',
|
||||
adminPhone: '',
|
||||
userIdError: '',
|
||||
phoneError: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 获取部门列表
|
||||
const fetchDepartments = async () => {
|
||||
try {
|
||||
departmentManagement.loading = true
|
||||
departmentManagement.error = null
|
||||
const departments = await departmentApi.getDepartments()
|
||||
departmentManagement.departments = departments
|
||||
} catch (error) {
|
||||
console.error('获取部门列表失败:', error)
|
||||
departmentManagement.error = '获取部门列表失败'
|
||||
} finally {
|
||||
departmentManagement.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 打开添加部门模态框
|
||||
const showAddDepartmentModal = () => {
|
||||
departmentManagement.modalTitle = '添加部门'
|
||||
departmentManagement.editMode = false
|
||||
departmentManagement.editDepartmentId = null
|
||||
departmentManagement.form = {
|
||||
name: '',
|
||||
description: '',
|
||||
adminUserId: '',
|
||||
adminPassword: '',
|
||||
adminConfirmPassword: '',
|
||||
adminPhone: '',
|
||||
userIdError: '',
|
||||
phoneError: ''
|
||||
}
|
||||
departmentManagement.modalVisible = true
|
||||
}
|
||||
|
||||
// 打开编辑部门模态框
|
||||
const showEditDepartmentModal = (department) => {
|
||||
departmentManagement.modalTitle = '编辑部门'
|
||||
departmentManagement.editMode = true
|
||||
departmentManagement.editDepartmentId = department.id
|
||||
departmentManagement.form = {
|
||||
name: department.name,
|
||||
description: department.description || '',
|
||||
adminUserId: '',
|
||||
adminPassword: '',
|
||||
adminConfirmPassword: '',
|
||||
adminPhone: '',
|
||||
userIdError: '',
|
||||
phoneError: ''
|
||||
}
|
||||
departmentManagement.modalVisible = true
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const validatePhoneNumber = (phone) => {
|
||||
if (!phone) {
|
||||
return true // 手机号可选
|
||||
}
|
||||
const phoneRegex = /^1[3-9]\d{9}$/
|
||||
return phoneRegex.test(phone)
|
||||
}
|
||||
|
||||
// 监听手机号输入变化
|
||||
watch(
|
||||
() => departmentManagement.form.adminPhone,
|
||||
(newPhone) => {
|
||||
departmentManagement.form.phoneError = ''
|
||||
if (newPhone && !validatePhoneNumber(newPhone)) {
|
||||
departmentManagement.form.phoneError = '请输入正确的手机号格式'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 检查管理员用户ID是否可用
|
||||
const checkAdminUserId = async () => {
|
||||
const userId = departmentManagement.form.adminUserId.trim()
|
||||
departmentManagement.form.userIdError = ''
|
||||
|
||||
if (!userId) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证格式
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(userId)) {
|
||||
departmentManagement.form.userIdError = '用户ID只能包含字母、数字和下划线'
|
||||
return
|
||||
}
|
||||
|
||||
if (userId.length < 3 || userId.length > 20) {
|
||||
departmentManagement.form.userIdError = '用户ID长度必须在3-20个字符之间'
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
try {
|
||||
const result = await apiSuperAdminGet(`/api/auth/check-user-id/${userId}`)
|
||||
if (!result.is_available) {
|
||||
departmentManagement.form.userIdError = '该用户ID已被使用'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查用户ID失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理部门表单提交
|
||||
const handleDepartmentFormSubmit = async () => {
|
||||
try {
|
||||
// 验证部门名称
|
||||
if (!departmentManagement.form.name.trim()) {
|
||||
notification.error({ message: '部门名称不能为空' })
|
||||
return
|
||||
}
|
||||
|
||||
if (departmentManagement.form.name.trim().length < 2) {
|
||||
notification.error({ message: '部门名称至少2个字符' })
|
||||
return
|
||||
}
|
||||
|
||||
// 验证管理员用户ID
|
||||
const adminUserId = departmentManagement.form.adminUserId.trim()
|
||||
if (!adminUserId) {
|
||||
notification.error({ message: '请输入管理员用户ID' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(adminUserId)) {
|
||||
notification.error({ message: '用户ID只能包含字母、数字和下划线' })
|
||||
return
|
||||
}
|
||||
|
||||
if (adminUserId.length < 3 || adminUserId.length > 20) {
|
||||
notification.error({ message: '用户ID长度必须在3-20个字符之间' })
|
||||
return
|
||||
}
|
||||
|
||||
if (departmentManagement.form.userIdError) {
|
||||
notification.error({ message: '管理员用户ID已存在或格式错误' })
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!departmentManagement.form.adminPassword) {
|
||||
notification.error({ message: '请输入管理员密码' })
|
||||
return
|
||||
}
|
||||
|
||||
if (departmentManagement.form.adminPassword !== departmentManagement.form.adminConfirmPassword) {
|
||||
notification.error({ message: '两次输入的密码不一致' })
|
||||
return
|
||||
}
|
||||
|
||||
// 验证手机号
|
||||
if (departmentManagement.form.adminPhone && !validatePhoneNumber(departmentManagement.form.adminPhone)) {
|
||||
notification.error({ message: '请输入正确的手机号格式' })
|
||||
return
|
||||
}
|
||||
|
||||
departmentManagement.loading = true
|
||||
|
||||
if (departmentManagement.editMode) {
|
||||
// 更新部门
|
||||
await departmentApi.updateDepartment(departmentManagement.editDepartmentId, {
|
||||
name: departmentManagement.form.name.trim(),
|
||||
description: departmentManagement.form.description.trim() || undefined
|
||||
})
|
||||
notification.success({ message: '部门更新成功' })
|
||||
} else {
|
||||
// 创建部门,同时创建管理员
|
||||
await departmentApi.createDepartment({
|
||||
name: departmentManagement.form.name.trim(),
|
||||
description: departmentManagement.form.description.trim() || undefined,
|
||||
admin_user_id: adminUserId,
|
||||
admin_password: departmentManagement.form.adminPassword,
|
||||
admin_phone: departmentManagement.form.adminPhone || undefined
|
||||
})
|
||||
|
||||
notification.success({ message: `部门创建成功,管理员 "${adminUserId}" 已创建` })
|
||||
}
|
||||
|
||||
// 重新获取部门列表
|
||||
await fetchDepartments()
|
||||
departmentManagement.modalVisible = false
|
||||
} catch (error) {
|
||||
console.error('部门操作失败:', error)
|
||||
notification.error({
|
||||
message: '操作失败',
|
||||
description: error.message || '请稍后重试'
|
||||
})
|
||||
} finally {
|
||||
departmentManagement.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除部门
|
||||
const confirmDeleteDepartment = (department) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除部门',
|
||||
content: `确定要删除部门 "${department.name}" 吗?此操作不可撤销。部门下必须没有用户才能删除。`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
try {
|
||||
departmentManagement.loading = true
|
||||
await departmentApi.deleteDepartment(department.id)
|
||||
notification.success({ message: '部门删除成功' })
|
||||
// 重新获取部门列表
|
||||
await fetchDepartments()
|
||||
} catch (error) {
|
||||
console.error('删除部门失败:', error)
|
||||
notification.error({
|
||||
message: '删除失败',
|
||||
description: error.message || '请稍后重试'
|
||||
})
|
||||
} finally {
|
||||
departmentManagement.loading = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 在组件挂载时获取部门列表
|
||||
onMounted(() => {
|
||||
fetchDepartments()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.department-management {
|
||||
margin-top: 12px;
|
||||
min-height: 50vh;
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
color: var(--gray-600);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-section {
|
||||
overflow: hidden;
|
||||
|
||||
.error-message {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.department-table {
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: var(--gray-50);
|
||||
font-weight: 500;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.department-name {
|
||||
.name-text {
|
||||
font-weight: 500;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
}
|
||||
|
||||
.description-text {
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--gray-25);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.department-modal {
|
||||
:deep(.ant-modal-header) {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--gray-150);
|
||||
|
||||
.ant-modal-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-modal-body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.department-form {
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
padding-bottom: 4px;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: var(--color-error-500);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
color: var(--gray-600);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -27,7 +27,6 @@
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold',
|
||||
flexShrink: 0,
|
||||
padding: '2px 4px',
|
||||
borderRadius: '3px'
|
||||
}"
|
||||
:title="getModelStatusTooltip(name)"
|
||||
|
||||
@ -650,7 +650,8 @@ onMounted(() => {
|
||||
background: var(--gray-0);
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
padding: 10px 16px;
|
||||
padding-bottom: 8px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
<div class="card-header">
|
||||
<div class="provider-info">
|
||||
<h4>{{ provider.name }}</h4>
|
||||
<span class="provider-id">{{ providerId }}</span>
|
||||
<span class="provider-id">ID: {{ providerId }}</span>
|
||||
</div>
|
||||
<div class="provider-actions">
|
||||
<a-button
|
||||
@ -856,10 +856,8 @@ const testCustomProvider = async (providerId, modelName) => {
|
||||
}
|
||||
|
||||
.provider-id {
|
||||
background: var(--main-color);
|
||||
color: var(--gray-0);
|
||||
color: var(--gray-600);
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@ -40,11 +40,20 @@
|
||||
<UserOutlined class="icon" />
|
||||
<span>用户管理</span>
|
||||
</div>
|
||||
<div
|
||||
class="sider-item"
|
||||
:class="{ activesec: activeTab === 'department' }"
|
||||
@click="activeTab = 'department'"
|
||||
v-if="userStore.isSuperAdmin"
|
||||
>
|
||||
<TeamOutlined class="icon" />
|
||||
<span>部门管理</span>
|
||||
</div>
|
||||
<div
|
||||
class="sider-item"
|
||||
:class="{ activesec: activeTab === 'mcp' }"
|
||||
@click="activeTab = 'mcp'"
|
||||
v-if="userStore.isAdmin"
|
||||
v-if="userStore.isSuperAdmin"
|
||||
>
|
||||
<ApiOutlined class="icon" />
|
||||
<span>MCP 管理</span>
|
||||
@ -81,10 +90,18 @@
|
||||
class="nav-item"
|
||||
:class="{ active: activeTab === 'mcp' }"
|
||||
@click="activeTab = 'mcp'"
|
||||
v-if="userStore.isAdmin"
|
||||
v-if="userStore.isSuperAdmin"
|
||||
>
|
||||
MCP 管理
|
||||
</div>
|
||||
<div
|
||||
class="nav-item"
|
||||
:class="{ active: activeTab === 'department' }"
|
||||
@click="activeTab = 'department'"
|
||||
v-if="userStore.isSuperAdmin"
|
||||
>
|
||||
部门管理
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
@ -102,9 +119,13 @@
|
||||
<UserManagementComponent />
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'mcp'" v-if="userStore.isAdmin">
|
||||
<div v-show="activeTab === 'mcp'" v-if="userStore.isSuperAdmin">
|
||||
<McpServersComponent />
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'department'" v-if="userStore.isSuperAdmin">
|
||||
<DepartmentManagementComponent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -114,11 +135,12 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { SettingOutlined, CodeOutlined, UserOutlined, ApiOutlined } from '@ant-design/icons-vue'
|
||||
import { SettingOutlined, CodeOutlined, UserOutlined, ApiOutlined, TeamOutlined } from '@ant-design/icons-vue'
|
||||
import BasicSettingsSection from '@/components/BasicSettingsSection.vue'
|
||||
import ModelProvidersComponent from '@/components/ModelProvidersComponent.vue'
|
||||
import UserManagementComponent from '@/components/UserManagementComponent.vue'
|
||||
import McpServersComponent from '@/components/McpServersComponent.vue'
|
||||
import DepartmentManagementComponent from '@/components/DepartmentManagementComponent.vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
|
||||
@ -142,6 +142,10 @@
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item" v-if="userStore.departmentId">
|
||||
<div class="info-label">部门</div>
|
||||
<div class="info-value">{{ userStore.departmentName || '默认部门' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作区域 -->
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
<div v-else class="user-cards-grid">
|
||||
<div v-for="user in userManagement.users" :key="user.id" class="user-card">
|
||||
<div class="card-header">
|
||||
<div class="user-info">
|
||||
<div class="user-info-main">
|
||||
<div class="user-avatar">
|
||||
<img
|
||||
v-if="user.avatar"
|
||||
@ -38,14 +38,28 @@
|
||||
{{ user.username.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-basic-info">
|
||||
<h4 class="username">{{ user.username }}</h4>
|
||||
<div class="user-id">ID: {{ user.user_id || '-' }}</div>
|
||||
<div class="user-info-content">
|
||||
<div class="name-tag-row">
|
||||
<h4 class="username">{{ user.username }}</h4>
|
||||
<div class="tags-row">
|
||||
<a-tag
|
||||
v-if="userStore.isSuperAdmin && user.department_name"
|
||||
class="dept-tag small-tag"
|
||||
>
|
||||
{{ user.department_name }}
|
||||
</a-tag>
|
||||
<a-tooltip :title="getRoleLabel(user.role)">
|
||||
<span class="role-icon-wrapper" :class="getRoleClass(user.role)">
|
||||
<UserLock v-if="user.role === 'superadmin'" :size="16" />
|
||||
<UserStar v-else-if="user.role === 'admin'" :size="16" />
|
||||
<User v-else :size="16" />
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-id-row">ID: {{ user.user_id || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-tag :color="getRoleColor(user.role)" class="role-tag">
|
||||
{{ getRoleLabel(user.role) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
@ -190,6 +204,15 @@
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 部门选择器(仅超级管理员可见) -->
|
||||
<a-form-item v-if="userStore.isSuperAdmin" label="部门" class="form-item">
|
||||
<a-select v-model:value="userManagement.form.departmentId" size="large" placeholder="请选择部门">
|
||||
<a-select-option v-for="dept in departmentManagement.departments" :key="dept.id" :value="dept.id">
|
||||
{{ dept.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
@ -199,7 +222,9 @@
|
||||
import { reactive, onMounted, watch } from 'vue'
|
||||
import { notification, Modal } from 'ant-design-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { departmentApi } from '@/apis'
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { User, UserLock, UserStar } from 'lucide-vue-next'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const userStore = useUserStore()
|
||||
@ -220,12 +245,29 @@ const userManagement = reactive({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
role: 'user', // 默认角色
|
||||
departmentId: null, // 部门ID
|
||||
usernameError: '', // 用户名错误信息
|
||||
phoneError: '' // 手机号错误信息
|
||||
},
|
||||
displayPasswordFields: true // 编辑时是否显示密码字段
|
||||
})
|
||||
|
||||
// 部门列表(仅超级管理员使用)
|
||||
const departmentManagement = reactive({
|
||||
departments: []
|
||||
})
|
||||
|
||||
// 获取部门列表
|
||||
const fetchDepartments = async () => {
|
||||
if (!userStore.isSuperAdmin) return // 普通管理员不需要获取所有部门列表
|
||||
try {
|
||||
const departments = await departmentApi.getDepartments()
|
||||
departmentManagement.departments = departments
|
||||
} catch (error) {
|
||||
console.error('获取部门列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加验证用户名并生成user_id的函数
|
||||
const validateAndGenerateUserId = async () => {
|
||||
const username = userManagement.form.username.trim()
|
||||
@ -316,6 +358,7 @@ const showAddUserModal = () => {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
role: 'user', // 默认角色为普通用户
|
||||
departmentId: null,
|
||||
usernameError: '',
|
||||
phoneError: ''
|
||||
}
|
||||
@ -335,6 +378,7 @@ const showEditUserModal = (user) => {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
role: user.role,
|
||||
departmentId: user.department_id || null,
|
||||
usernameError: '',
|
||||
phoneError: ''
|
||||
}
|
||||
@ -393,6 +437,11 @@ const handleUserFormSubmit = async () => {
|
||||
updateData.phone_number = userManagement.form.phoneNumber
|
||||
}
|
||||
|
||||
// 超级管理员可以修改部门
|
||||
if (userStore.isSuperAdmin && userManagement.form.departmentId) {
|
||||
updateData.department_id = userManagement.form.departmentId
|
||||
}
|
||||
|
||||
// 如果显示了密码字段并且填写了密码,才更新密码
|
||||
if (userManagement.displayPasswordFields && userManagement.form.password) {
|
||||
updateData.password = userManagement.form.password
|
||||
@ -408,6 +457,11 @@ const handleUserFormSubmit = async () => {
|
||||
role: userManagement.form.role
|
||||
}
|
||||
|
||||
// 超级管理员可以指定部门
|
||||
if (userStore.isSuperAdmin && userManagement.form.departmentId) {
|
||||
createData.department_id = userManagement.form.departmentId
|
||||
}
|
||||
|
||||
// 添加手机号字段(如果填写了)
|
||||
if (userManagement.form.phoneNumber) {
|
||||
createData.phone_number = userManagement.form.phoneNumber
|
||||
@ -494,9 +548,23 @@ const getRoleColor = (role) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleClass = (role) => {
|
||||
switch (role) {
|
||||
case 'superadmin':
|
||||
return 'role-superadmin'
|
||||
case 'admin':
|
||||
return 'role-admin'
|
||||
case 'user':
|
||||
return 'role-user'
|
||||
default:
|
||||
return 'role-default'
|
||||
}
|
||||
}
|
||||
|
||||
// 在组件挂载时获取用户列表
|
||||
onMounted(() => {
|
||||
fetchUsers()
|
||||
onMounted(async () => {
|
||||
await fetchUsers()
|
||||
await fetchDepartments()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -546,7 +614,9 @@ onMounted(() => {
|
||||
background: var(--gray-0);
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
padding: 12px 16px;
|
||||
padding-bottom: 6px;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
@ -556,15 +626,12 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.user-info {
|
||||
.user-info-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
@ -590,33 +657,72 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.user-basic-info {
|
||||
.username {
|
||||
margin: 0 0 2px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
.user-info-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.name-tag-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.username {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tags-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.role-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.role-superadmin {
|
||||
color: var(--color-error-700);
|
||||
background: var(--color-error-50);
|
||||
}
|
||||
&.role-admin {
|
||||
color: var(--color-info-700);
|
||||
background: var(--color-info-50);
|
||||
}
|
||||
&.role-user {
|
||||
color: var(--color-success-700);
|
||||
background: var(--color-success-50);
|
||||
}
|
||||
}
|
||||
|
||||
.small-tag {
|
||||
font-size: 12px;
|
||||
height: 22px;
|
||||
padding: 0 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-id {
|
||||
.user-id-row {
|
||||
font-size: 12px;
|
||||
color: var(--gray-600);
|
||||
color: var(--gray-500);
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.role-tag {
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -656,7 +762,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
padding-top: 8px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid var(--gray-25);
|
||||
|
||||
.action-btn {
|
||||
|
||||
@ -10,6 +10,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
const phoneNumber = ref('')
|
||||
const avatar = ref('')
|
||||
const userRole = ref('')
|
||||
const departmentId = ref(null)
|
||||
const departmentName = ref('')
|
||||
|
||||
// 计算属性
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
@ -53,6 +55,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
phoneNumber.value = data.phone_number || ''
|
||||
avatar.value = data.avatar || ''
|
||||
userRole.value = data.role
|
||||
departmentId.value = data.department_id || null
|
||||
departmentName.value = data.department_name || ''
|
||||
|
||||
// 只保存 token 到本地存储
|
||||
localStorage.setItem('user_token', data.access_token)
|
||||
@ -73,6 +77,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
phoneNumber.value = ''
|
||||
avatar.value = ''
|
||||
userRole.value = ''
|
||||
departmentId.value = null
|
||||
departmentName.value = ''
|
||||
|
||||
// 只清除 token
|
||||
localStorage.removeItem('user_token')
|
||||
@ -103,6 +109,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
phoneNumber.value = data.phone_number || ''
|
||||
avatar.value = data.avatar || ''
|
||||
userRole.value = data.role
|
||||
departmentId.value = data.department_id || null
|
||||
departmentName.value = data.department_name || ''
|
||||
|
||||
// 只保存 token 到本地存储
|
||||
localStorage.setItem('user_token', data.access_token)
|
||||
@ -296,6 +304,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
phoneNumber.value = userData.phone_number || ''
|
||||
avatar.value = userData.avatar || ''
|
||||
userRole.value = userData.role
|
||||
departmentId.value = userData.department_id || null
|
||||
departmentName.value = '' // 部门名称通过 departmentId 获取
|
||||
|
||||
return userData
|
||||
} catch (error) {
|
||||
@ -347,6 +357,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
phoneNumber,
|
||||
avatar,
|
||||
userRole,
|
||||
departmentId,
|
||||
departmentName,
|
||||
|
||||
// 计算属性
|
||||
isLoggedIn,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user