feat(users): 用户管理表格内直接分配组织(超管)
## 起因
用户指出「所属组织要可以分配」。核查后发现两件事:
1. **组织没丢** —— 生产 46 个组织、28 个项目、68 个柜子都在;
2. **但 5 个用户里 0 个被分到组织**,所以那一列全是 `-`。
其中 2 个是总管理员(按规则本就不属组织,正确),
另 3 个(李四/企业管理员、张三/王五/操作员)**从未分配**,违反
「操作员必须有企业归属」的规则。
原流程要「点编辑图标 → 弹窗里选组织 → 保存」三步才能改一个用户,
3 个用户要重复 3 次。现改成**表格里直接分配**。
## 改动
`users.tsx` 的「所属组织」列:
- 超管 + 非总管理员 ⇒ 渲染成内联 `Select`,选完即存(`PUT /users/:id` 只传
`organization_id`,后端 `UpdateUserInput` 本就支持,无需改后端);
- 总管理员行仍显示 `-`(后端 `resolve_update_org_id` 对 `role=2` 强制 `None`);
- 非超管操作者仍只读。
顺带修 `api/user.ts`:`fetchUsers` 的参数类型漏了 `role`(上一轮加了角色过滤
但类型没跟上,靠结构兼容蒙混过关)。
## 实测(测试环境,临时账号用完即删)
```
PUT 只传 organization_id → HTTP 200,库中 organization_id 已更新 OK
改分配到另一组织 → HTTP 200,库中已更新 OK
只改名不传组织 → 组织保持原值,未被清空 OK
给总管理员分组织 → HTTP 200 但库中仍为 None(被强制忽略) OK
列表 organization_name → 正确返回组织名 OK
```
## ⚠️ 两个我自己出的岔子(都已修正)
1. **断言写错**:我原以为「给总管理员分组织」应返回 400,实测返回 200。
读库确认 `organization_id` 仍是 `None` —— 后端是**静默忽略**而非拒绝,
行为符合规则。是测试预期错,不是代码错。
2. **测试脚本留了脏数据**:临时用户删不掉,因为 `operation_logs` 有外键引用
(`log_operation` 写了日志)。已补删 `operation_logs` → `user_roles` → `users`
清理干净,测试库恢复 1 用户 / 1 组织 / 1 项目 / 1 柜子。
## 待用户决定(生产数据,我不擅自改)
李四 / 张三 / 王五 三个账号要分到哪个组织?生产有 46 个组织可选。
现在超管可以在表格里直接点选分配,你也可以告诉我分配方案我来执行。
## 验收
- web `npm run typecheck`:exit 0
- `npm run build`:exit 0
- 已发布生产(web 已更新,0 ERROR、域名 401/200)
This commit is contained in:
parent
0441d91d1f
commit
f3f841b1f0
@ -47,11 +47,14 @@ export async function fetchUsers(params: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
/** 角色过滤:0=操作员 1=企业管理员 2=总管理员;不给 = 全部 */
|
||||
role?: number;
|
||||
}): Promise<UserListData> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.page) searchParams.set('page', String(params.page));
|
||||
if (params.page_size) searchParams.set('page_size', String(params.page_size));
|
||||
if (params.keyword) searchParams.set('keyword', params.keyword);
|
||||
if (params.role !== undefined) searchParams.set('role', String(params.role));
|
||||
const res = await api.get<UserListData>(`/users?${searchParams.toString()}`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@ -339,6 +339,19 @@ export default function UsersPage() {
|
||||
setScopeModalVisible(true);
|
||||
};
|
||||
|
||||
/** 表格内直接分配组织(仅超管可见/可操作)。
|
||||
* 不必进编辑弹窗 —— 生产上有 3 个用户从未分过组织,逐个进弹窗改太慢。 */
|
||||
const handleAssignOrg = async (user: User, orgId: number) => {
|
||||
try {
|
||||
await updateUser(user.id, { organization_id: orgId });
|
||||
Message.success(`已将「${user.name || user.phone}」分配到该组织`);
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '分配失败');
|
||||
fetchData(); // 失败也刷新,把下拉回滚到真实值
|
||||
}
|
||||
};
|
||||
|
||||
/** 表格列 */
|
||||
const columns: TableColumnProps<User>[] = [
|
||||
{ title: '手机号', dataIndex: 'phone', width: 130 },
|
||||
@ -347,8 +360,29 @@ export default function UsersPage() {
|
||||
{
|
||||
title: '所属组织',
|
||||
dataIndex: 'organization_name',
|
||||
width: 150,
|
||||
render: (v: string | null) => v || '-',
|
||||
width: 180,
|
||||
render: (v: string | null, record: User) => {
|
||||
// 总管理员本就不属任何组织(后端 resolve_update_org_id 对 role=2 强制 None);
|
||||
// 非超管操作者也只能看,不能改。
|
||||
if (!isSuperAdmin || record.role === 2) {
|
||||
return v || '-';
|
||||
}
|
||||
// 超管 + 非总管理员 ⇒ 表格里直接分配(生产上有用户从未分过组织)
|
||||
return (
|
||||
<Select
|
||||
size="mini"
|
||||
value={record.organization_id ?? undefined}
|
||||
placeholder="未分配"
|
||||
style={{ width: 150 }}
|
||||
options={orgs.map((o) => ({
|
||||
label: o.name,
|
||||
value: o.id,
|
||||
disabled: o.disabled === 1,
|
||||
}))}
|
||||
onChange={(val) => handleAssignOrg(record, val as number)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user