From 0a776c1cf8d5410dd08559fd32eaaeaf0c74abe8 Mon Sep 17 00:00:00 2001 From: chengkai3 Date: Fri, 19 Jun 2026 23:06:15 +0800 Subject: [PATCH] =?UTF-8?q?fix:[FL-119][=E8=8F=9C=E5=8D=95=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=8A=B6=E6=80=81=E5=88=87=E6=8D=A2=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E7=BB=9F=E4=B8=80]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- memory/2026-06-19.md | 18 ++++++ web/src/app/admin/menus/page.tsx | 98 +++++++++++++++----------------- 2 files changed, 63 insertions(+), 53 deletions(-) create mode 100644 memory/2026-06-19.md diff --git a/memory/2026-06-19.md b/memory/2026-06-19.md new file mode 100644 index 0000000..401b6b0 --- /dev/null +++ b/memory/2026-06-19.md @@ -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 其他行为。 diff --git a/web/src/app/admin/menus/page.tsx b/web/src/app/admin/menus/page.tsx index 86e793e..9e77c9f 100644 --- a/web/src/app/admin/menus/page.tsx +++ b/web/src/app/admin/menus/page.tsx @@ -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([]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [deletingMenuId, setDeletingMenuId] = useState(null); + const [updatingStatusMenuId, setUpdatingStatusMenuId] = useState(null); const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); const [dialogOpen, setDialogOpen] = useState(false); const [editingMenuId, setEditingMenuId] = useState(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>(() => { const base: TableColumnsType = [ { 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() { -