diff --git a/server/routers/__init__.py b/server/routers/__init__.py index 706a92eb..fa25fe84 100644 --- a/server/routers/__init__.py +++ b/server/routers/__init__.py @@ -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/* diff --git a/server/routers/auth_router.py b/server/routers/auth_router.py index 9a35c4c0..3b0f49c7 100644 --- a/server/routers/auth_router.py +++ b/server/routers/auth_router.py @@ -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() # 记录操作 diff --git a/server/routers/department_router.py b/server/routers/department_router.py new file mode 100644 index 00000000..4de846a4 --- /dev/null +++ b/server/routers/department_router.py @@ -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": "部门已删除"} diff --git a/server/utils/migrate.py b/server/utils/migrate.py index b5d4143d..0a912245 100644 --- a/server/utils/migrate.py +++ b/server/utils/migrate.py @@ -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, diff --git a/src/config/app.py b/src/config/app.py index 9439f147..e42e243c 100644 --- a/src/config/app.py +++ b/src/config/app.py @@ -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", diff --git a/src/storage/db/models.py b/src/storage/db/models.py index 6606410a..a7e77171 100644 --- a/src/storage/db/models.py +++ b/src/storage/db/models.py @@ -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, diff --git a/web/src/apis/department_api.js b/web/src/apis/department_api.js new file mode 100644 index 00000000..319790df --- /dev/null +++ b/web/src/apis/department_api.js @@ -0,0 +1,69 @@ +/** + * 部门管理 API + */ + +import { + apiSuperAdminGet, + apiSuperAdminPost, + apiSuperAdminPut, + apiSuperAdminDelete +} from './base' + +const BASE_URL = '/api/departments' + +/** + * 获取部门列表 + * @returns {Promise} 部门列表 + */ +export const getDepartments = () => { + return apiSuperAdminGet(BASE_URL) +} + +/** + * 获取部门详情 + * @param {number} departmentId - 部门ID + * @returns {Promise} 部门详情 + */ +export const getDepartment = (departmentId) => { + return apiSuperAdminGet(`${BASE_URL}/${departmentId}`) +} + +/** + * 创建部门 + * @param {Object} data - 部门数据 + * @param {string} data.name - 部门名称 + * @param {string} [data.description] - 部门描述 + * @returns {Promise} 创建的部门 + */ +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} 更新后的部门 + */ +export const updateDepartment = (departmentId, data) => { + return apiSuperAdminPut(`${BASE_URL}/${departmentId}`, data) +} + +/** + * 删除部门 + * @param {number} departmentId - 部门ID + * @returns {Promise} 删除结果 + */ +export const deleteDepartment = (departmentId) => { + return apiSuperAdminDelete(`${BASE_URL}/${departmentId}`) +} + +export const departmentApi = { + getDepartments, + getDepartment, + createDepartment, + updateDepartment, + deleteDepartment +} diff --git a/web/src/apis/index.js b/web/src/apis/index.js index efed8807..08d66638 100644 --- a/web/src/apis/index.js +++ b/web/src/apis/index.js @@ -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 { diff --git a/web/src/components/BasicSettingsSection.vue b/web/src/components/BasicSettingsSection.vue index 94d495cb..edf646bb 100644 --- a/web/src/components/BasicSettingsSection.vue +++ b/web/src/components/BasicSettingsSection.vue @@ -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); diff --git a/web/src/components/DepartmentManagementComponent.vue b/web/src/components/DepartmentManagementComponent.vue new file mode 100644 index 00000000..59611961 --- /dev/null +++ b/web/src/components/DepartmentManagementComponent.vue @@ -0,0 +1,558 @@ + + + + + diff --git a/web/src/components/EmbeddingModelSelector.vue b/web/src/components/EmbeddingModelSelector.vue index 244fa1f8..cf933027 100644 --- a/web/src/components/EmbeddingModelSelector.vue +++ b/web/src/components/EmbeddingModelSelector.vue @@ -27,7 +27,6 @@ fontSize: '11px', fontWeight: 'bold', flexShrink: 0, - padding: '2px 4px', borderRadius: '3px' }" :title="getModelStatusTooltip(name)" diff --git a/web/src/components/McpServersComponent.vue b/web/src/components/McpServersComponent.vue index 47911425..0066a723 100644 --- a/web/src/components/McpServersComponent.vue +++ b/web/src/components/McpServersComponent.vue @@ -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); diff --git a/web/src/components/ModelProvidersComponent.vue b/web/src/components/ModelProvidersComponent.vue index 79551ce0..9d112d98 100644 --- a/web/src/components/ModelProvidersComponent.vue +++ b/web/src/components/ModelProvidersComponent.vue @@ -27,7 +27,7 @@

{{ provider.name }}

- {{ providerId }} + ID: {{ providerId }}
{ } .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; } diff --git a/web/src/components/SettingsModal.vue b/web/src/components/SettingsModal.vue index 33089cdc..57d2004a 100644 --- a/web/src/components/SettingsModal.vue +++ b/web/src/components/SettingsModal.vue @@ -40,11 +40,20 @@ 用户管理
+
+ + 部门管理 +
MCP 管理 @@ -81,10 +90,18 @@ class="nav-item" :class="{ active: activeTab === 'mcp' }" @click="activeTab = 'mcp'" - v-if="userStore.isAdmin" + v-if="userStore.isSuperAdmin" > MCP 管理
+
@@ -102,9 +119,13 @@ -
+
+ +
+ +
@@ -114,11 +135,12 @@ @@ -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 { diff --git a/web/src/stores/user.js b/web/src/stores/user.js index fb30407b..ca11a00b 100644 --- a/web/src/stores/user.js +++ b/web/src/stores/user.js @@ -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,