fix:[FL-119][菜单管理状态切换接口统一]
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -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 其他行为。
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user