fix:[FL-119][菜单管理状态切换接口统一]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-19 23:06:15 +08:00
parent 8084561d10
commit 0a776c1cf8
2 changed files with 63 additions and 53 deletions
+18
View File
@@ -0,0 +1,18 @@
# Work Log - 菜单状态切换接口统一(FL-119)
- 背景:
- 菜单管理页创建、编辑、删除均使用 `/api/v1/admin/menus*`,但表格和卡片视图的启用/禁用状态切换仍调用 `/api/menus/{id}`,接口资源族不一致。
- 本次处理:
-`web/src/app/admin/menus/page.tsx` 新增统一的菜单状态切换函数,表格与卡片视图复用同一逻辑。
- 状态切换请求统一为 `PATCH /api/v1/admin/menus/{id}`payload 仅包含 `{ status }`
- 新增行级 `updatingStatusMenuId` busy 状态,状态切换期间禁用对应行/卡片操作。
- 状态切换成功后刷新菜单列表,并通过统一 toast 状态反馈成功/失败,避免重复提示或残留旧错误。
- 验证:
- 基线:`npm --workspace web exec eslint src/app/admin/menus/page.tsx` 通过,存在 3 条既有 warning。
- 修改后:`npm --workspace web exec eslint src/app/admin/menus/page.tsx` 通过,仅剩 1 条既有 pagination hook warning。
- 修改后:`npm --workspace web exec tsc --noEmit` 通过。
- 风险与关注点:
- 改动仅影响菜单管理页状态切换的前端请求路径、复用逻辑和行级 busy 状态,不改变后端接口、字段结构或菜单 CRUD 其他行为。
+45 -53
View File
@@ -26,7 +26,7 @@ import {
type MenuProps,
type TableColumnsType,
} from "antd";
import { MoreOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons";
import { MoreOutlined, EditOutlined } from "@ant-design/icons";
import type { CSSProperties, ComponentType } from "react";
import { useAuth } from "@/components/auth-provider";
@@ -91,13 +91,15 @@ function normalizeMenuItemPath(menu: MenuItem): MenuItem {
export default function AdminMenusPage() {
const { user, initializing, fetchWithAuth, hasPermission } = useAuth();
const { message: messageApi, modal } = App.useApp();
const { message: messageApi } = App.useApp();
const isMobile = useMobileDetection();
const [menus, setMenus] = useState<MenuItem[]>([]);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [deletingMenuId, setDeletingMenuId] = useState<string | null>(null);
const [updatingStatusMenuId, setUpdatingStatusMenuId] = useState<string | null>(null);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
const [editingMenuId, setEditingMenuId] = useState<string | null>(null);
const [keyword, setKeyword] = useState("");
@@ -120,7 +122,9 @@ export default function AdminMenusPage() {
useToastFeedback({
errorMessage: error,
successMessage: success,
clearError: () => setError(""),
clearSuccess: () => setSuccess(""),
});
const parentOptions = useMemo(
@@ -369,6 +373,7 @@ export default function AdminMenusPage() {
const removeMenu = useCallback(async (menu: MenuItem) => {
setDeletingMenuId(menu.id);
setError("");
setSuccess("");
try {
const response = await fetchWithAuth(`/api/v1/admin/menus/${menu.id}`, {
@@ -391,6 +396,34 @@ export default function AdminMenusPage() {
}
}, [closeDialog, editingMenuId, fetchWithAuth, loadMenus, messageApi]);
const updateMenuStatus = useCallback(async (menu: MenuItem) => {
const nextStatus: "enabled" | "disabled" = menu.status === "enabled" ? "disabled" : "enabled";
setUpdatingStatusMenuId(menu.id);
setError("");
setSuccess("");
try {
const response = await fetchWithAuth(`/api/v1/admin/menus/${menu.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: nextStatus }),
});
if (!response.ok) {
throw new Error(await readApiError(response));
}
const payload = await loadMenus();
if (payload) {
setSuccess(nextStatus === "enabled" ? "菜单已启用" : "菜单已禁用");
}
} catch (candidate) {
setError(candidate instanceof Error ? candidate.message : "菜单状态更新失败");
} finally {
setUpdatingStatusMenuId(null);
}
}, [fetchWithAuth, loadMenus]);
const columns = useMemo<TableColumnsType<MenuItem>>(() => {
const base: TableColumnsType<MenuItem> = [
{ title: "ID", dataIndex: "id", width: 110 },
@@ -441,8 +474,9 @@ export default function AdminMenusPage() {
fixed: "right",
width: 180,
render: (_, record) => {
const updatingLoading = updatingStatusMenuId === record.id;
const deleteLoading = deletingMenuId === record.id;
const menuBusy = deleteLoading;
const menuBusy = updatingLoading || deleteLoading;
const moreMenuItems: MenuProps["items"] = [
{
@@ -461,29 +495,7 @@ export default function AdminMenusPage() {
key: "toggle-status",
label: record.status === "enabled" ? "禁用" : "启用",
disabled: menuBusy,
onClick: async () => {
try {
const response = await fetchWithAuth(`/api/menus/${record.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
status: record.status === "enabled" ? "disabled" : "enabled",
}),
});
if (!response.ok) {
const msg = await readApiError(response);
setError(msg);
messageApi.error(msg);
return;
}
messageApi.success("菜单状态已更新");
await loadMenus();
} catch (err) {
const msg = err instanceof Error ? err.message : "更新失败";
setError(msg);
messageApi.error(msg);
}
},
onClick: () => updateMenuStatus(record),
},
];
@@ -507,7 +519,7 @@ export default function AdminMenusPage() {
</Popconfirm>
<Dropdown menu={{ items: moreMenuItems }} trigger={["click"]}>
<Button size="small" disabled={menuBusy} icon={<MoreOutlined />} />
<Button size="small" loading={updatingLoading} disabled={menuBusy} icon={<MoreOutlined />} />
</Dropdown>
</Space>
);
@@ -515,10 +527,12 @@ export default function AdminMenusPage() {
});
return base;
}, [canManage, deletingMenuId, fetchWithAuth, loadMenus, menuNameById, messageApi, removeMenu, setError, startEdit]);
}, [canManage, deletingMenuId, menuNameById, removeMenu, startEdit, updateMenuStatus, updatingStatusMenuId]);
const renderMenuCard = (menuItem: MenuItem) => {
const menuBusy = deletingMenuId === menuItem.id;
const updatingLoading = updatingStatusMenuId === menuItem.id;
const deleteLoading = deletingMenuId === menuItem.id;
const menuBusy = updatingLoading || deleteLoading;
const moreMenuItems: MenuProps["items"] = [
{
@@ -537,29 +551,7 @@ export default function AdminMenusPage() {
key: "toggle-status",
label: menuItem.status === "enabled" ? "禁用" : "启用",
disabled: menuBusy || !canManage,
onClick: async () => {
try {
const response = await fetchWithAuth(`/api/menus/${menuItem.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
status: menuItem.status === "enabled" ? "disabled" : "enabled",
}),
});
if (!response.ok) {
const msg = await readApiError(response);
setError(msg);
messageApi.error(msg);
return;
}
messageApi.success("菜单状态已更新");
await loadMenus();
} catch (err) {
const msg = err instanceof Error ? err.message : "更新失败";
setError(msg);
messageApi.error(msg);
}
},
onClick: () => updateMenuStatus(menuItem),
},
];
@@ -585,7 +577,7 @@ export default function AdminMenusPage() {
onClick={() => startEdit(menuItem)}
/>
<Dropdown menu={{ items: moreMenuItems }} trigger={["click"]}>
<Button type="text" size="small" disabled={menuBusy} icon={<MoreOutlined />} />
<Button type="text" size="small" loading={updatingLoading} disabled={menuBusy} icon={<MoreOutlined />} />
</Dropdown>
</Space>
) : null