deeafe239f
前端: - 新增 Pinia theme store,localStorage 持久化,data-theme 切换 - App.vue: :root 新增 --font-*/--sidebar-* CSS 变量;新增 :root[data-theme=light] 完整配色(极简黑白金 + Noto Sans SC) - DesktopLayout: 侧边栏 15 个变量化 + 顶栏主题切换按钮 - MobileLayout: 装饰色变量化 - Settings: 新增「界面主题」卡片 - 21 个组件 font-family 统一替换为 var(--font-*) - 新增 utils/managerColor.ts — 12 色调色板 + 自适应文字色 + 后端自定义色优先 - WorkPlans/MiniBusiness/KeyVisits/CustomerManage: 客户经理标签色统一走共享工具 - UserManage: 编辑弹窗新增颜色选择器(仅 manager) - LightBoard: 去标题星标图标 - ManagerWorkspace/WorkPlans/MiniBusiness/KeyVisits: 删除按钮改为实心红底白字 后端: - User 模型新增 color 列 + lifespan 自动迁移 - users API 支持 color 字段读写 - UserOut schema 新增 color Co-Authored-By: Claude <noreply@anthropic.com>
149 lines
4.6 KiB
Python
149 lines
4.6 KiB
Python
import uuid
|
|
from typing import Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from app.database import get_db
|
|
from app.middleware.auth import get_current_user, require_director
|
|
from app.models.user import User
|
|
from app.schemas.customer import UserOut
|
|
|
|
router = APIRouter(prefix="/users", tags=["Users"])
|
|
|
|
|
|
class UpdateUserRoleRequest(BaseModel):
|
|
role: str # manager / director / leader
|
|
department: str = ""
|
|
require_report: bool = True
|
|
color: Optional[str] = None # hex color for manager tags
|
|
|
|
|
|
@router.get("/", response_model=list[UserOut])
|
|
async def list_users(
|
|
role: Optional[str] = Query(None),
|
|
current_user: dict = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List all users. Used for companion selection, assignment, etc."""
|
|
query = select(User)
|
|
if role:
|
|
query = query.where(User.role == role)
|
|
query = query.order_by(User.name)
|
|
result = await db.execute(query)
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.get("/me")
|
|
async def get_current_user_info(
|
|
current_user: dict = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Get current user's full profile."""
|
|
result = await db.execute(select(User).where(User.id == uuid.UUID(current_user["user_id"])))
|
|
user = result.scalar_one_or_none()
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
return {
|
|
"id": str(user.id),
|
|
"casdoor_id": user.casdoor_id,
|
|
"name": user.name,
|
|
"role": user.role,
|
|
"department": user.department,
|
|
"wecom_userid": user.wecom_userid,
|
|
}
|
|
|
|
|
|
@router.put("/{user_id}/role")
|
|
async def update_user_role(
|
|
user_id: str,
|
|
data: UpdateUserRoleRequest,
|
|
current_user: dict = Depends(require_director),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Director updates a user's role. Only director can do this."""
|
|
if data.role not in ("manager", "director", "leader"):
|
|
raise HTTPException(status_code=400, detail="Invalid role. Must be manager/director/leader")
|
|
|
|
result = await db.execute(select(User).where(User.id == uuid.UUID(user_id)))
|
|
user = result.scalar_one_or_none()
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
user.role = data.role
|
|
if data.department:
|
|
user.department = data.department
|
|
user.require_report = data.require_report
|
|
if data.color is not None:
|
|
user.color = data.color
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
|
|
return {
|
|
"id": str(user.id),
|
|
"name": user.name,
|
|
"role": user.role,
|
|
"department": user.department,
|
|
"require_report": user.require_report,
|
|
"color": user.color,
|
|
}
|
|
|
|
|
|
class UpdateWecomRequest(BaseModel):
|
|
wecom_userid: Optional[str] = None # None or "" to unbind
|
|
|
|
|
|
@router.put("/{user_id}/wecom")
|
|
async def update_user_wecom(
|
|
user_id: str,
|
|
data: UpdateWecomRequest,
|
|
current_user: dict = Depends(require_director),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Director binds or unbinds a user's WeChat Work account."""
|
|
result = await db.execute(select(User).where(User.id == uuid.UUID(user_id)))
|
|
user = result.scalar_one_or_none()
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
new_id = (data.wecom_userid or "").strip()
|
|
|
|
# Check duplicate
|
|
if new_id:
|
|
dup = await db.execute(
|
|
select(User).where(User.wecom_userid == new_id, User.id != uuid.UUID(user_id))
|
|
)
|
|
existing = dup.scalar_one_or_none()
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail=f"该企微ID已被「{existing.name}」绑定")
|
|
|
|
user.wecom_userid = new_id if new_id else None
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
|
|
return {
|
|
"id": str(user.id),
|
|
"name": user.name,
|
|
"wecom_userid": user.wecom_userid,
|
|
}
|
|
|
|
|
|
@router.delete("/{user_id}")
|
|
async def delete_user(
|
|
user_id: str,
|
|
current_user: dict = Depends(require_director),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Director deletes a user. Cannot delete self."""
|
|
if user_id == current_user["user_id"]:
|
|
raise HTTPException(status_code=400, detail="不能删除自己")
|
|
|
|
result = await db.execute(select(User).where(User.id == uuid.UUID(user_id)))
|
|
user = result.scalar_one_or_none()
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
await db.delete(user)
|
|
await db.commit()
|
|
return {"detail": "deleted", "name": user.name}
|