diff --git a/memory/2026-06-20.md b/memory/2026-06-20.md index 957184c..e0c1901 100644 --- a/memory/2026-06-20.md +++ b/memory/2026-06-20.md @@ -39,6 +39,29 @@ - 风险与关注点: - 改动仅迁移菜单管理页前端数据管理架构,不改变 `/api/v1/admin/menus*` 接口路径、请求字段或权限语义。 +## Follow-up - 菜单管理页细节一致性补齐(FL-151) + +- 背景: + - 复查继续指出菜单管理页在筛选宽度、移动卡片间距、编辑弹窗标题、导入顺序、表格列类型、状态命名和筛选语义等细节上仍未完全对齐用户管理页。 + +- 本次处理: + - 菜单页 import 顺序调整为 React Query、Ant Design、Icons、`antd/es/table` 类型、Next Link、React hooks 的顺序。 + - 表格列类型改为 `ColumnsType`,并将 `columns` 从 `useMemo` 改为普通常量数组。 + - 关键词输入 state 从 `keyword` 改为 `keywordInput`,桌面关键词宽度改为 `260px`。 + - 状态筛选改为 `undefined` 表示“全部”,对齐用户页的 `statusFilter` 语义。 + - 移动端卡片容器移除显式 `mt-4`,对齐用户页间距处理。 + - 编辑菜单弹窗标题补充当前菜单名称和 ID;移动端删除确认改为下拉项内联 `Modal.confirm`。 + - 权限变量定义顺序调整为先 `canManage`,后 `canRead`。 + +- 验证: + - 基线:`npm --workspace web exec eslint src/app/admin/menus/page.tsx` 通过。 + - 基线:`npm --workspace web exec tsc --noEmit` 因既有 `src/app/admin/elevation-records/page.tsx` 类型错误失败,菜单页无报错。 + - 修改后:`npm --workspace web exec eslint src/app/admin/menus/page.tsx` 通过。 + - 修改后:`npm --workspace web exec tsc --noEmit --pretty false` 仍仅因既有 `src/app/admin/elevation-records/page.tsx` 类型错误失败,菜单页无报错。 + +- 风险与关注点: + - 改动仅涉及菜单管理页前端一致性细节,不改变后端接口、权限判断或菜单 CRUD 请求语义。 + # Work Log - 角色管理页对齐用户管理页规范(FL-152) - 背景: diff --git a/web/src/app/admin/menus/page.tsx b/web/src/app/admin/menus/page.tsx index d1cc002..a92766e 100644 --- a/web/src/app/admin/menus/page.tsx +++ b/web/src/app/admin/menus/page.tsx @@ -1,8 +1,6 @@ "use client"; -import Link from "next/link"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button, Card, @@ -24,10 +22,11 @@ import { Typography, type CardProps, type MenuProps, - type TableColumnsType, } from "antd"; import { MoreOutlined, EditOutlined } from "@ant-design/icons"; -import type { CSSProperties, ComponentType, RefAttributes } from "react"; +import type { ColumnsType } from "antd/es/table"; +import Link from "next/link"; +import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ComponentType, type RefAttributes } from "react"; import { useAuth } from "@/components/auth-provider"; import { useToastFeedback } from "@/hooks/use-toast-feedback"; @@ -39,8 +38,6 @@ import type { MenuItem, MenuListResponse } from "@/types/auth"; const AntCard = Card as unknown as ComponentType>; -type FilterStatus = "all" | "enabled" | "disabled"; - type MenuFormValues = { code: string; name: string; @@ -113,8 +110,8 @@ export default function AdminMenusPage() { const [success, setSuccess] = useState(""); const [dialogOpen, setDialogOpen] = useState(false); const [editingMenuId, setEditingMenuId] = useState(null); - const [keyword, setKeyword] = useState(""); - const [statusFilter, setStatusFilter] = useState("all"); + const [keywordInput, setKeywordInput] = useState(""); + const [statusFilter, setStatusFilter] = useState<"enabled" | "disabled" | undefined>(undefined); const [searchKeyword, setSearchKeyword] = useState(""); const [pagination, setPagination] = useState({ current: 1, pageSize: 20 }); const [tableScrollY, setTableScrollY] = useState(MENU_TABLE_MIN_SCROLL_Y); @@ -129,8 +126,8 @@ export default function AdminMenusPage() { const paginationCurrent = pagination.current; const paginationPageSize = pagination.pageSize; - const canRead = hasPermission("menu.read") || hasPermission("menu.manage"); const canManage = hasPermission("menu.manage"); + const canRead = hasPermission("menu.read") || hasPermission("menu.manage"); const trimmedKeyword = searchKeyword.trim(); const menusQueryParams = useMemo(() => { @@ -140,7 +137,7 @@ export default function AdminMenusPage() { if (trimmedKeyword) { params.set("keyword", trimmedKeyword); } - if (statusFilter !== "all") { + if (statusFilter) { params.set("status", statusFilter); } return params.toString(); @@ -200,6 +197,10 @@ export default function AdminMenusPage() { return compareMenuIds(a.id, b.id); }); }, [menus]); + const editingMenu = useMemo( + () => menus.find((menu) => menu.id === editingMenuId) ?? null, + [editingMenuId, menus], + ); useTopicSubscription( "admin.menus", @@ -274,7 +275,7 @@ export default function AdminMenusPage() { }, [allLoadedMenus.length, menusQuery.isLoading, isLoadingMore, menuTotal, viewMode]); const handleKeywordChange = (value: string) => { - setKeyword(value); + setKeywordInput(value); if (keywordDebounceTimeoutRef.current) { clearTimeout(keywordDebounceTimeoutRef.current); @@ -288,8 +289,8 @@ export default function AdminMenusPage() { }, 500); }; - const handleStatusFilterChange = (value?: Exclude) => { - setStatusFilter(value ?? "all"); + const handleStatusFilterChange = (value?: "enabled" | "disabled") => { + setStatusFilter(value); setPagination((prev) => ({ ...prev, current: 1 })); setCardViewPage(1); setAllLoadedMenus([]); @@ -456,66 +457,49 @@ export default function AdminMenusPage() { } }, [createMenuMutation, editingMenuId, form, updateMenuMutation]); - const confirmRemoveMenu = useCallback((menu: MenuItem) => { - Modal.confirm({ - title: `确认删除菜单 ${menu.name}(${menu.code})?`, - okText: "删除", - cancelText: "取消", - okButtonProps: { danger: true }, - onOk: () => deleteMenuMutation.mutate(menu.id), - }); - }, [deleteMenuMutation]); - const updateMenuStatus = useCallback(async (menu: MenuItem) => { const nextStatus: "enabled" | "disabled" = menu.status === "enabled" ? "disabled" : "enabled"; updateMenuMutation.mutate({ menuId: menu.id, payload: { status: nextStatus } }); }, [updateMenuMutation]); - const columns = useMemo>(() => { - const base: TableColumnsType = [ - { title: "ID", dataIndex: "id", width: 110 }, - { - title: "编码", - dataIndex: "code", - width: 220, - render: (value: string) => {value}, - }, - { - title: "名称", - dataIndex: "name", - width: 180, - }, - { - title: "路径", - dataIndex: "path", - width: 220, - render: (value: string | null) => value || "-", - }, - { - title: "父菜单", - dataIndex: "parent_id", - width: 220, - render: (value: string | null) => (value ? menuNameById.get(value) ?? value : "-"), - }, - { - title: "状态", - dataIndex: "status", - width: 110, - render: (value: string) => - value === "enabled" ? 已启用 : 已禁用, - }, - { - title: "排序", - dataIndex: "sort_order", - width: 90, - }, - ]; - - if (!canManage) { - return base; - } - - base.push({ + const columns: ColumnsType = [ + { title: "ID", dataIndex: "id", width: 110 }, + { + title: "编码", + dataIndex: "code", + width: 220, + render: (value: string) => {value}, + }, + { + title: "名称", + dataIndex: "name", + width: 180, + }, + { + title: "路径", + dataIndex: "path", + width: 220, + render: (value: string | null) => value || "-", + }, + { + title: "父菜单", + dataIndex: "parent_id", + width: 220, + render: (value: string | null) => (value ? menuNameById.get(value) ?? value : "-"), + }, + { + title: "状态", + dataIndex: "status", + width: 110, + render: (value: string) => + value === "enabled" ? 已启用 : 已禁用, + }, + { + title: "排序", + dataIndex: "sort_order", + width: 90, + }, + ...(canManage ? [{ title: "操作", key: "actions", fixed: "right", @@ -559,10 +543,8 @@ export default function AdminMenusPage() { ); }, - }); - - return base; - }, [canManage, deleteMenuMutation, deletingMenuId, menuNameById, startEdit, updateMenuStatus, updatingStatusMenuId]); + } satisfies ColumnsType[number]] : []), + ]; const renderMenuCard = (menuItem: MenuItem) => { const updatingLoading = updatingStatusMenuId === menuItem.id; @@ -575,7 +557,15 @@ export default function AdminMenusPage() { label: "删除", danger: true, disabled: menuBusy || !canManage, - onClick: () => confirmRemoveMenu(menuItem), + onClick: () => { + Modal.confirm({ + title: `确认删除菜单 ${menuItem.name}(${menuItem.code})?`, + okText: "删除", + cancelText: "取消", + okButtonProps: { danger: true }, + onOk: () => deleteMenuMutation.mutate(menuItem.id), + }); + }, }, { key: "toggle-status", @@ -770,7 +760,7 @@ export default function AdminMenusPage() { handleKeywordChange(event.currentTarget.value)} placeholder="按编码/名称/路径筛选" /> @@ -778,18 +768,18 @@ export default function AdminMenusPage() { ) : (
- + handleKeywordChange(event.currentTarget.value)} placeholder="按编码/名称/路径筛选" /> - > - value={statusFilter === "all" ? undefined : statusFilter} + + value={statusFilter} allowClear placeholder="全部" onChange={handleStatusFilterChange} @@ -839,7 +829,7 @@ export default function AdminMenusPage() { /> ) : ( -
+
{menusQuery.isLoading && allLoadedMenus.length === 0 ? (
@@ -879,7 +869,7 @@ export default function AdminMenusPage() { void submit()}