Files
duxingchen 9598c3fb9f feat(分组): 成员改为标签云展示 + 添加成员支持多选批量
1) 成员不再一人一行
   原先用 antd Table:成员一多就要滚很久,右侧还大片留白。
   改为标签云(flex-wrap),一行能放好几个。
   交互一并简化:
     · 点标签正文 → 切换组长(悬停有提示)
     · 点 ×     → 移出分组
     · 无权限时两者都不出现,退化成纯展示
   组长用金色标签 + 皇冠图标,一眼可辨。

2) 添加成员支持多选
   Select 加 mode="multiple" + maxTagCount="responsive",可搜可多选、批量提交。
   后端 POST /groups/{id}/members 由单个 user_id 改为 user_ids 数组,
   返回 {added, skipped, skipped_names}:
     · 已在组内的**静默跳过**而非整批 409 —— 多选时难免选中已在组里的人,
       为此让整批失败体验很差;前端会把跳过的名单说清楚,
       否则用户会疑惑「明明选了 5 个,怎么只多了 3 个」
     · 批量里混入超管则**整批拒绝并指名道姓**,不制造「部分成功」这种
       难以解释的中间状态

实测(后端):
  批量加 3 人           → added 3
  重复提交(2 旧 1 新) → added 1 / skipped 2,整批仍成功
  混入超管              → 400「以下账号是超级管理员,业务分组对其不生效:sunxia」
  空列表                → 422
  测试成员已清理
2026-09-21 17:44:06 +08:00

79 lines
2.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""业务分组 Schemas"""
from pydantic import BaseModel, Field
class PhaseOption(BaseModel):
"""可选的生命周期阶段 —— 由服务端下发,前端不要写死这两个值"""
value: str
label: str
class GroupMemberOut(BaseModel):
user_id: str # MOM 短账号(与 Task.assignee_id 同口径)
display_name: str
is_leader: bool
class GroupOut(BaseModel):
id: int
name: str
parent_id: int | None = None
parent_name: str | None = None
description: str | None = None
sort_order: int = 0
is_active: bool = True
phases: list[str] = [] # 该组自己的可见范围(不含继承)
effective_phases: list[str] = [] # 实际生效范围(自己没配则取父组的)
phase_labels: list[str] = [] # 生效范围的中文标签
member_count: int = 0
# ---- 当前用户对这个组能做什么(服务端算好,前端别自己推导)----
can_manage_members: bool = False # 能加/移成员、设组长
can_manage_group: bool = False # 能改组名/范围/启停/删除 —— 仅超管
is_my_leader: bool = False # 我是这个组的组长
class GroupDetailOut(GroupOut):
members: list[GroupMemberOut] = []
class GroupCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=50, description="分组显示名")
parent_id: int | None = Field(None, description="上级大组ID为空则建的是大组")
description: str | None = Field(None, max_length=200)
sort_order: int = 0
phases: list[str] = Field(default_factory=list, description="可见的生命周期阶段")
class GroupUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=50)
parent_id: int | None = None
description: str | None = Field(None, max_length=200)
sort_order: int | None = None
is_active: bool | None = None
phases: list[str] | None = Field(
None, description="整组覆盖式更新;传 [] 表示清空(此时继承父组)",
)
class GroupMembersAdd(BaseModel):
"""批量加成员 —— 界面是多选下拉,一次提交一批"""
user_ids: list[str] = Field(
..., min_length=1, max_length=200, description="要加入的 MOM 账号列表",
)
is_leader: bool = False
class GroupMembersAddResult(BaseModel):
added: list["GroupMemberOut"] = [] # 本次真正加进去的
skipped: list[str] = [] # 已在组内、被跳过的账号
skipped_names: list[str] = [] # 上面那些账号的中文名,便于前端提示
class GroupMemberUpdate(BaseModel):
is_leader: bool
class MemberCandidate(BaseModel):
username: str # 短账号,存进组的就是它
full_name: str # 真实姓名,仅供界面展示