feat(account): 添加部门基础管理功能 #480

- 实现数据库迁移,创建部门表并向用户表添加department_id字段。
- 创建Department模型并与User模型建立关联关系。
- 开发部门管理API以支持增删改查操作。
- 添加DepartmentManagementComponent用于在UI中管理部门。
- 在UserManagementComponent中集成部门选择功能以进行用户分配。
- 更新用户存储以处理部门数据。
- 增强UI组件以改善样式和用户体验。
This commit is contained in:
Wenjie Zhang 2026-01-18 22:54:43 +08:00
parent 62af84eea9
commit 5b3703ac1c
17 changed files with 1277 additions and 52 deletions

View File

@ -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/*

View File

@ -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()
# 记录操作

View 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": "部门已删除"}

View File

@ -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,

View File

@ -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",

View File

@ -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,

View 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
}

View File

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

View File

@ -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);

View 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="请输入管理员用户ID3-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>

View File

@ -27,7 +27,6 @@
fontSize: '11px',
fontWeight: 'bold',
flexShrink: 0,
padding: '2px 4px',
borderRadius: '3px'
}"
:title="getModelStatusTooltip(name)"

View File

@ -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);

View File

@ -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;
}

View File

@ -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: {

View File

@ -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>
<!-- 操作区域 -->

View File

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

View File

@ -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,