@@ -972,3 +972,23 @@
|
||||
- 当 `public.user_role` 存在:继续走 legacy 表链路。
|
||||
- 当 `public.user_role` 缺失:自动回退到 modern 表链路,并补齐 `role_permissions + permissions` 权限码。
|
||||
- 该兼容仅针对角色读取相关接口(`list_roles/get_role_by_id/list_role_menu_ids`);角色写入链路暂仍以 legacy 表为准。
|
||||
|
||||
## 用户管理检索与分页口径(2026-05-01)
|
||||
|
||||
- 用户管理列表接口 `GET /api/v1/users` 支持查询参数:
|
||||
- `limit` / `offset`:分页
|
||||
- `keyword`:按 `user_id/email/username` 模糊检索
|
||||
- `status`:状态过滤(`active|enabled|disabled`)
|
||||
- 后端统计口径要求:`total` 必须与当前检索/过滤条件一致(不是全量总数)。
|
||||
- 前端 `/admin/users` 采用“检索条件 + 分页状态”驱动请求,不再固定拉取 `limit=200` 全量列表。
|
||||
|
||||
## 用户管理编辑口径(2026-05-01)
|
||||
|
||||
- 用户信息更新接口 `PATCH /api/v1/users/{user_id}` 当前支持:
|
||||
- `email`
|
||||
- `username`
|
||||
- `status`
|
||||
- 更新规则:
|
||||
- `email` 入库前统一 `trim + lower`,且做唯一性校验;
|
||||
- `username` 入库前统一 `trim`,且做唯一性校验;
|
||||
- 空字符串视为非法更新(返回失败)。
|
||||
|
||||
@@ -45,10 +45,12 @@ def create_user_account(
|
||||
def list_all_users(
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
keyword: str | None = Query(default=None, max_length=128),
|
||||
status_filter: str | None = Query(default=None, alias="status"),
|
||||
_: CurrentUser = Depends(require_permission("user.manage")),
|
||||
db: Session = Depends(get_db),
|
||||
) -> UserListResponse:
|
||||
return list_users(db, limit=limit, offset=offset)
|
||||
return list_users(db, limit=limit, offset=offset, keyword=keyword, status=status_filter)
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserPublic)
|
||||
@@ -81,7 +83,7 @@ def update_user_profile(
|
||||
if not updated:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found or username exists",
|
||||
detail="User not found or email/username exists",
|
||||
)
|
||||
return updated
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class UserListResponse(BaseModel):
|
||||
|
||||
|
||||
class UserUpdateRequest(BaseModel):
|
||||
email: str | None = None
|
||||
username: str | None = Field(default=None, min_length=3, max_length=64)
|
||||
status: Literal["active", "disabled", "enabled"] | None = None
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import and_, bindparam, func, select, text
|
||||
from sqlalchemy import and_, bindparam, func, or_, select, text
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session, object_session
|
||||
|
||||
@@ -33,14 +33,40 @@ def _user_with_rbac_stmt():
|
||||
return select(User)
|
||||
|
||||
|
||||
def list_users(db: Session, *, limit: int, offset: int) -> UserListResponse:
|
||||
total = db.scalar(select(func.count()).select_from(User)) or 0
|
||||
stmt = (
|
||||
_user_with_rbac_stmt()
|
||||
.order_by(User.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
def list_users(
|
||||
db: Session,
|
||||
*,
|
||||
limit: int,
|
||||
offset: int,
|
||||
keyword: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> UserListResponse:
|
||||
conditions = []
|
||||
normalized_keyword = (keyword or "").strip()
|
||||
if normalized_keyword:
|
||||
like = f"%{normalized_keyword}%"
|
||||
conditions.append(
|
||||
or_(
|
||||
User.id.ilike(like),
|
||||
User.email.ilike(like),
|
||||
User.username.ilike(like),
|
||||
)
|
||||
)
|
||||
normalized_status = (status or "").strip().lower()
|
||||
if normalized_status in {"active", "enabled"}:
|
||||
conditions.append(User.status.in_(["active", "ACTIVE", "ENABLED"]))
|
||||
elif normalized_status == "disabled":
|
||||
conditions.append(User.status.in_(["disabled", "DISABLED", "INACTIVE"]))
|
||||
|
||||
total_stmt = select(func.count()).select_from(User)
|
||||
if conditions:
|
||||
total_stmt = total_stmt.where(*conditions)
|
||||
total = db.scalar(total_stmt) or 0
|
||||
|
||||
stmt = _user_with_rbac_stmt().order_by(User.created_at.desc())
|
||||
if conditions:
|
||||
stmt = stmt.where(*conditions)
|
||||
stmt = stmt.offset(offset).limit(limit)
|
||||
users = db.execute(stmt).unique().scalars().all()
|
||||
return UserListResponse(items=[serialize_user(user) for user in users], total=total)
|
||||
|
||||
@@ -168,13 +194,29 @@ def update_user(
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if payload.username and payload.username != user.username:
|
||||
duplicate = db.scalar(
|
||||
select(User.id).where(User.username == payload.username, User.id != user.id)
|
||||
)
|
||||
if duplicate:
|
||||
if payload.email is not None:
|
||||
next_email = payload.email.strip().lower()
|
||||
if not next_email:
|
||||
return None
|
||||
user.username = payload.username
|
||||
if next_email != user.email:
|
||||
duplicate = db.scalar(
|
||||
select(User.id).where(User.email == next_email, User.id != user.id)
|
||||
)
|
||||
if duplicate:
|
||||
return None
|
||||
user.email = next_email
|
||||
|
||||
if payload.username is not None:
|
||||
next_username = payload.username.strip()
|
||||
if not next_username:
|
||||
return None
|
||||
if next_username != user.username:
|
||||
duplicate = db.scalar(
|
||||
select(User.id).where(User.username == next_username, User.id != user.id)
|
||||
)
|
||||
if duplicate:
|
||||
return None
|
||||
user.username = next_username
|
||||
|
||||
status_changed = False
|
||||
if payload.status:
|
||||
|
||||
@@ -206,3 +206,43 @@
|
||||
- 风险与影响:
|
||||
- 影响范围限定在后台角色读取接口(`/api/v1/admin/roles*`)查询链路。
|
||||
- 对 legacy 表完整的数据库行为保持不变;仅在 `user_role` 缺失时触发 modern 回退逻辑。
|
||||
|
||||
## Work Log - 用户管理优化(编辑/检索/分页)(2026-05-01)
|
||||
|
||||
- 背景:
|
||||
- Issue `FL-121` 要求用户管理支持:用户信息修改、用户检索、用户表格分页。
|
||||
|
||||
- 本次改动(最小闭环):
|
||||
- 后端 `users` 列表接口增强(检索 + 分页参数联动):
|
||||
- 文件:`api/app/api/v1/users.py`
|
||||
- `GET /api/v1/users` 新增查询参数:
|
||||
- `keyword`:按 `user_id/email/username` 模糊检索
|
||||
- `status`:按启用/禁用状态过滤
|
||||
- 后端用户服务增强:
|
||||
- 文件:`api/app/services/user_service.py`
|
||||
- `list_users(...)` 支持 `keyword/status` 条件查询并对 `total` 同步计数。
|
||||
- `update_user(...)` 支持修改 `email` 与 `username`:
|
||||
- 统一 trim/归一化
|
||||
- 重复值冲突校验
|
||||
- 空值保护
|
||||
- 用户更新请求模型补齐:
|
||||
- 文件:`api/app/schemas/user.py`
|
||||
- `UserUpdateRequest` 新增可选字段 `email`。
|
||||
- 前端用户管理页增强:
|
||||
- 文件:`web/src/app/admin/users/page.tsx`
|
||||
- 新增“用户检索”区:关键字输入 + 状态筛选 + 搜索/重置。
|
||||
- 用户列表请求改为受查询条件驱动(query key 包含分页/筛选参数)。
|
||||
- 表格接入分页器(页码、每页条数、总数联动后端)。
|
||||
- 新增“编辑用户”弹窗,支持修改邮箱/用户名/状态。
|
||||
- 编辑提交仅传变更字段,避免无效更新。
|
||||
|
||||
- 验证(未执行编译/构建,遵循当前任务约束):
|
||||
- 代码走读与关键路径自检:
|
||||
- `GET /api/v1/users` -> 支持 `limit/offset/keyword/status`。
|
||||
- `PATCH /api/v1/users/{id}` -> 支持 `email/username/status` 更新。
|
||||
- 前端列表查询参数与分页状态联动一致。
|
||||
|
||||
- 风险与影响:
|
||||
- 影响范围集中在用户管理模块(`/admin/users` 与 `/api/v1/users*`)。
|
||||
- 旧调用方不传 `keyword/status` 时行为保持兼容。
|
||||
- 更新失败错误提示文案仍共用“not found or email/username exists”,后续如需更精确错误码可再拆分。
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Input,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
@@ -40,6 +41,12 @@ type CreateUserValues = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
type EditUserValues = {
|
||||
email: string;
|
||||
username: string;
|
||||
status: "active" | "disabled";
|
||||
};
|
||||
|
||||
type ResetPasswordValues = {
|
||||
password: string;
|
||||
};
|
||||
@@ -55,6 +62,7 @@ export default function AdminUsersPage() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [createForm] = Form.useForm<CreateUserValues>();
|
||||
const [editUserForm] = Form.useForm<EditUserValues>();
|
||||
const [resetPasswordForm] = Form.useForm<ResetPasswordValues>();
|
||||
|
||||
const [savingUserId, setSavingUserId] = useState<string | null>(null);
|
||||
@@ -62,7 +70,12 @@ export default function AdminUsersPage() {
|
||||
const [resettingUserId, setResettingUserId] = useState<string | null>(null);
|
||||
const [updatingStatusUserId, setUpdatingStatusUserId] = useState<string | null>(null);
|
||||
const [createUserModalOpen, setCreateUserModalOpen] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<UserPublic | null>(null);
|
||||
const [resetPasswordTarget, setResetPasswordTarget] = useState<UserPublic | null>(null);
|
||||
const [keywordInput, setKeywordInput] = useState("");
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<"all" | "active" | "disabled">("all");
|
||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 20 });
|
||||
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
@@ -70,14 +83,27 @@ export default function AdminUsersPage() {
|
||||
const canManage = hasPermission("user.manage");
|
||||
const canReadRoles = hasPermission("role.read") || hasPermission("role.manage");
|
||||
|
||||
const usersPath = "/api/v1/users?limit=200&offset=0";
|
||||
const trimmedKeyword = searchKeyword.trim();
|
||||
const usersQueryParams = useMemo(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("limit", String(pagination.pageSize));
|
||||
params.set("offset", String((pagination.current - 1) * pagination.pageSize));
|
||||
if (trimmedKeyword) {
|
||||
params.set("keyword", trimmedKeyword);
|
||||
}
|
||||
if (statusFilter !== "all") {
|
||||
params.set("status", statusFilter);
|
||||
}
|
||||
return params.toString();
|
||||
}, [pagination.current, pagination.pageSize, statusFilter, trimmedKeyword]);
|
||||
const usersPath = `/api/v1/users?${usersQueryParams}`;
|
||||
const rolesPath = "/api/v1/admin/roles";
|
||||
|
||||
const loadUsers = useCallback(async () => {
|
||||
const response = await fetchWithAuth(usersPath);
|
||||
if (!response.ok) throw new Error(await readApiError(response));
|
||||
return (await response.json()) as UserListResponse;
|
||||
}, [fetchWithAuth]);
|
||||
}, [fetchWithAuth, usersPath]);
|
||||
|
||||
const loadRoles = useCallback(async () => {
|
||||
const response = await fetchWithAuth(rolesPath);
|
||||
@@ -86,7 +112,7 @@ export default function AdminUsersPage() {
|
||||
}, [fetchWithAuth]);
|
||||
|
||||
const usersQuery = useQuery({
|
||||
queryKey: [usersPath],
|
||||
queryKey: ["admin.users", usersQueryParams],
|
||||
queryFn: loadUsers,
|
||||
enabled: !!user && canManage,
|
||||
});
|
||||
@@ -101,7 +127,7 @@ export default function AdminUsersPage() {
|
||||
"admin.users",
|
||||
useCallback(() => {
|
||||
if (!user || !canManage) return;
|
||||
void queryClient.invalidateQueries({ queryKey: [usersPath] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["admin.users"] });
|
||||
if (canReadRoles) {
|
||||
void queryClient.invalidateQueries({ queryKey: [rolesPath] });
|
||||
}
|
||||
@@ -136,7 +162,7 @@ export default function AdminUsersPage() {
|
||||
);
|
||||
|
||||
const refreshData = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: [usersPath] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin.users"] });
|
||||
if (canReadRoles) {
|
||||
await queryClient.invalidateQueries({ queryKey: [rolesPath] });
|
||||
}
|
||||
@@ -218,11 +244,18 @@ export default function AdminUsersPage() {
|
||||
});
|
||||
|
||||
const updateUserProfileMutation = useMutation({
|
||||
mutationFn: async ({ userId, status }: { userId: string; status: "active" | "disabled" }) => {
|
||||
mutationFn: async ({ userId, payload }: {
|
||||
userId: string;
|
||||
payload: {
|
||||
email?: string;
|
||||
username?: string;
|
||||
status?: "active" | "disabled";
|
||||
};
|
||||
}) => {
|
||||
const response = await fetchWithAuth(`/api/v1/users/${userId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status }),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) throw new Error(await readApiError(response));
|
||||
return response.json() as Promise<UserPublic>;
|
||||
@@ -233,12 +266,18 @@ export default function AdminUsersPage() {
|
||||
setSuccess("");
|
||||
},
|
||||
onSuccess: async (_, variables) => {
|
||||
setSuccess(variables.status === "active" ? "用户已启用" : "用户已禁用");
|
||||
if (variables.payload.status) {
|
||||
setSuccess(variables.payload.status === "active" ? "用户已启用" : "用户已禁用");
|
||||
} else {
|
||||
setSuccess("用户信息已更新");
|
||||
}
|
||||
setEditingUser(null);
|
||||
editUserForm.resetFields();
|
||||
await refreshData();
|
||||
},
|
||||
onError: (candidate) => {
|
||||
setSuccess("");
|
||||
setError(candidate instanceof Error ? candidate.message : "更新用户状态失败");
|
||||
setError(candidate instanceof Error ? candidate.message : "更新用户信息失败");
|
||||
},
|
||||
onSettled: () => setUpdatingStatusUserId(null),
|
||||
});
|
||||
@@ -309,6 +348,52 @@ export default function AdminUsersPage() {
|
||||
resetPasswordForm.resetFields();
|
||||
};
|
||||
|
||||
const openEditUserModal = (target: UserPublic) => {
|
||||
setError("");
|
||||
setSuccess("");
|
||||
setEditingUser(target);
|
||||
editUserForm.setFieldsValue({
|
||||
email: target.email,
|
||||
username: target.username,
|
||||
status: target.status === "disabled" ? "disabled" : "active",
|
||||
});
|
||||
};
|
||||
|
||||
const closeEditUserModal = () => {
|
||||
if (updateUserProfileMutation.isPending) return;
|
||||
setEditingUser(null);
|
||||
editUserForm.resetFields();
|
||||
};
|
||||
|
||||
const handleSubmitEditUser = (values: EditUserValues) => {
|
||||
if (!editingUser) return;
|
||||
const nextEmail = values.email.trim().toLowerCase();
|
||||
const nextUsername = values.username.trim();
|
||||
const nextStatus = values.status;
|
||||
|
||||
const payload: { email?: string; username?: string; status?: "active" | "disabled" } = {};
|
||||
if (nextEmail !== editingUser.email.toLowerCase()) {
|
||||
payload.email = nextEmail;
|
||||
}
|
||||
if (nextUsername !== editingUser.username) {
|
||||
payload.username = nextUsername;
|
||||
}
|
||||
if (nextStatus !== editingUser.status) {
|
||||
payload.status = nextStatus;
|
||||
}
|
||||
|
||||
if (Object.keys(payload).length === 0) {
|
||||
setSuccess("未检测到变更");
|
||||
closeEditUserModal();
|
||||
return;
|
||||
}
|
||||
|
||||
updateUserProfileMutation.mutate({
|
||||
userId: editingUser.id,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmitResetPassword = (values: ResetPasswordValues) => {
|
||||
if (!resetPasswordTarget) return;
|
||||
resetPasswordMutation.mutate({ userId: resetPasswordTarget.id, password: values.password });
|
||||
@@ -327,6 +412,18 @@ export default function AdminUsersPage() {
|
||||
createForm.resetFields();
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
setSearchKeyword(keywordInput);
|
||||
setPagination((prev) => ({ ...prev, current: 1 }));
|
||||
};
|
||||
|
||||
const handleResetSearch = () => {
|
||||
setKeywordInput("");
|
||||
setSearchKeyword("");
|
||||
setStatusFilter("all");
|
||||
setPagination((prev) => ({ ...prev, current: 1 }));
|
||||
};
|
||||
|
||||
const queryError =
|
||||
(usersQuery.error instanceof Error ? usersQuery.error.message : "")
|
||||
|| (rolesQuery.error instanceof Error ? rolesQuery.error.message : "");
|
||||
@@ -402,16 +499,16 @@ export default function AdminUsersPage() {
|
||||
fixed: "right",
|
||||
width: 260,
|
||||
render: (_value, row) => {
|
||||
const statusLoading = updatingStatusUserId === row.id;
|
||||
const updatingLoading = updatingStatusUserId === row.id;
|
||||
const resetLoading = resettingUserId === row.id;
|
||||
const deleteLoading = deletingUserId === row.id;
|
||||
const rowBusy = statusLoading || resetLoading || deleteLoading;
|
||||
const rowBusy = updatingLoading || resetLoading || deleteLoading;
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
<Button
|
||||
size="small"
|
||||
loading={statusLoading}
|
||||
loading={updatingLoading}
|
||||
disabled={rowBusy || row.id === user?.id}
|
||||
onClick={() => {
|
||||
if (row.id === user?.id) {
|
||||
@@ -419,12 +516,20 @@ export default function AdminUsersPage() {
|
||||
return;
|
||||
}
|
||||
const nextStatus: "active" | "disabled" = row.status === "active" ? "disabled" : "active";
|
||||
updateUserProfileMutation.mutate({ userId: row.id, status: nextStatus });
|
||||
updateUserProfileMutation.mutate({ userId: row.id, payload: { status: nextStatus } });
|
||||
}}
|
||||
>
|
||||
{row.status === "active" ? "禁用" : "启用"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
disabled={rowBusy}
|
||||
onClick={() => openEditUserModal(row)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
loading={resetLoading}
|
||||
@@ -490,6 +595,36 @@ export default function AdminUsersPage() {
|
||||
{anyError && <Alert type="error" message="操作失败" description={anyError} showIcon />}
|
||||
{success && <Alert type="success" message={success} showIcon />}
|
||||
|
||||
<AntCard
|
||||
title="用户检索"
|
||||
>
|
||||
<Space wrap>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="按用户ID/邮箱/用户名搜索"
|
||||
value={keywordInput}
|
||||
onChange={(event) => setKeywordInput(event.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
style={{ width: 320 }}
|
||||
/>
|
||||
<Select<"all" | "active" | "disabled">
|
||||
value={statusFilter}
|
||||
style={{ width: 160 }}
|
||||
options={[
|
||||
{ label: "全部状态", value: "all" },
|
||||
{ label: "启用", value: "active" },
|
||||
{ label: "禁用", value: "disabled" },
|
||||
]}
|
||||
onChange={(value) => {
|
||||
setStatusFilter(value);
|
||||
setPagination((prev) => ({ ...prev, current: 1 }));
|
||||
}}
|
||||
/>
|
||||
<Button type="primary" onClick={handleSearch}>搜索</Button>
|
||||
<Button onClick={handleResetSearch}>重置</Button>
|
||||
</Space>
|
||||
</AntCard>
|
||||
|
||||
<AntCard
|
||||
title="用户列表"
|
||||
extra={(
|
||||
@@ -506,7 +641,16 @@ export default function AdminUsersPage() {
|
||||
rowKey="id"
|
||||
dataSource={users}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
pagination={{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: usersQuery.data?.total ?? 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPagination({ current: page, pageSize });
|
||||
},
|
||||
}}
|
||||
size="middle"
|
||||
scroll={{ x: 1500 }}
|
||||
locale={{
|
||||
@@ -587,6 +731,58 @@ export default function AdminUsersPage() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={editingUser ? `编辑用户:${editingUser.username}(${editingUser.id})` : "编辑用户"}
|
||||
open={!!editingUser}
|
||||
destroyOnClose
|
||||
onCancel={closeEditUserModal}
|
||||
onOk={() => editUserForm.submit()}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
confirmLoading={updateUserProfileMutation.isPending}
|
||||
>
|
||||
<Form<EditUserValues>
|
||||
form={editUserForm}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmitEditUser}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
label="邮箱"
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: "请输入邮箱" },
|
||||
{ type: "email", message: "邮箱格式不正确" },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入邮箱" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="用户名"
|
||||
name="username"
|
||||
rules={[
|
||||
{ required: true, message: "请输入用户名" },
|
||||
{ min: 3, message: "用户名至少 3 位" },
|
||||
{ max: 64, message: "用户名不能超过 64 位" },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="状态"
|
||||
name="status"
|
||||
rules={[{ required: true, message: "请选择状态" }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "启用", value: "active" },
|
||||
{ label: "禁用", value: "disabled" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={resetPasswordTarget ? `重置密码:${resetPasswordTarget.username}(${resetPasswordTarget.id})` : "重置密码"}
|
||||
open={!!resetPasswordTarget}
|
||||
|
||||
Reference in New Issue
Block a user