|
@@ -0,0 +1,183 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * 菜单树形表格组件
|
|
|
|
|
+ */
|
|
|
|
|
+import { useState } from "react";
|
|
|
|
|
+import {
|
|
|
|
|
+ Table,
|
|
|
|
|
+ Flex,
|
|
|
|
|
+ Text,
|
|
|
|
|
+ Button,
|
|
|
|
|
+} from "@contentful/f36-components";
|
|
|
|
|
+import type { SysMenu } from "@/types/system";
|
|
|
|
|
+import { createColumns } from "../constants/columns";
|
|
|
|
|
+
|
|
|
|
|
+interface MenuTreeTableProps {
|
|
|
|
|
+ menuList: SysMenu[];
|
|
|
|
|
+ selectedRows: string[];
|
|
|
|
|
+ onSelectAll: (checked: boolean) => void;
|
|
|
|
|
+ onSelectRow: (id: string, checked: boolean) => void;
|
|
|
|
|
+ onEdit: (record: SysMenu) => void;
|
|
|
|
|
+ onDelete: (id: string) => void;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface MenuTreeRowProps {
|
|
|
|
|
+ menu: SysMenu;
|
|
|
|
|
+ level: number;
|
|
|
|
|
+ selectedRows: string[];
|
|
|
|
|
+ onSelectRow: (id: string, checked: boolean) => void;
|
|
|
|
|
+ onEdit: (record: SysMenu) => void;
|
|
|
|
|
+ onDelete: (id: string) => void;
|
|
|
|
|
+ columns: any[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 单个菜单行组件
|
|
|
|
|
+function MenuTreeRow({
|
|
|
|
|
+ menu,
|
|
|
|
|
+ level,
|
|
|
|
|
+ selectedRows,
|
|
|
|
|
+ onSelectRow,
|
|
|
|
|
+ onEdit,
|
|
|
|
|
+ onDelete,
|
|
|
|
|
+ columns,
|
|
|
|
|
+}: MenuTreeRowProps) {
|
|
|
|
|
+ const [expanded, setExpanded] = useState(true);
|
|
|
|
|
+ const hasChildren = menu.children && menu.children.length > 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 缩进样式
|
|
|
|
|
+ const indentStyle = {
|
|
|
|
|
+ paddingLeft: `${level * 20}px`,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Table.Row>
|
|
|
|
|
+ <Table.Cell>
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="checkbox"
|
|
|
|
|
+ checked={selectedRows.includes(menu.id)}
|
|
|
|
|
+ onChange={(e) => onSelectRow(menu.id, e.target.checked)}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Table.Cell>
|
|
|
|
|
+ {columns.map((col, index) => (
|
|
|
|
|
+ <Table.Cell key={col.accessor}>
|
|
|
|
|
+ {index === 0 ? (
|
|
|
|
|
+ // 第一列显示层级和展开/收起按钮
|
|
|
|
|
+ <Flex alignItems="center" style={indentStyle}>
|
|
|
|
|
+ {hasChildren && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="transparent"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ onClick={() => setExpanded(!expanded)}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ minWidth: "20px",
|
|
|
|
|
+ padding: "0 4px",
|
|
|
|
|
+ marginRight: "8px",
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {expanded ? "−" : "+"}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {!hasChildren && (
|
|
|
|
|
+ <span
|
|
|
|
|
+ style={{
|
|
|
|
|
+ display: "inline-block",
|
|
|
|
|
+ width: "20px",
|
|
|
|
|
+ marginRight: "8px",
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+ {col.render ? (
|
|
|
|
|
+ col.render(menu[col.accessor as keyof SysMenu], menu)
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Text>{String(menu[col.accessor as keyof SysMenu] || "")}</Text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Flex>
|
|
|
|
|
+ ) : col.render ? (
|
|
|
|
|
+ col.render(menu[col.accessor as keyof SysMenu], menu)
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ String(menu[col.accessor as keyof SysMenu] || "")
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Table.Cell>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Table.Row>
|
|
|
|
|
+ {/* 递归渲染子菜单 */}
|
|
|
|
|
+ {hasChildren &&
|
|
|
|
|
+ expanded &&
|
|
|
|
|
+ menu.children!.map((child) => (
|
|
|
|
|
+ <MenuTreeRow
|
|
|
|
|
+ key={child.id}
|
|
|
|
|
+ menu={child}
|
|
|
|
|
+ level={level + 1}
|
|
|
|
|
+ selectedRows={selectedRows}
|
|
|
|
|
+ onSelectRow={onSelectRow}
|
|
|
|
|
+ onEdit={onEdit}
|
|
|
|
|
+ onDelete={onDelete}
|
|
|
|
|
+ columns={columns}
|
|
|
|
|
+ />
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default function MenuTreeTable({
|
|
|
|
|
+ menuList,
|
|
|
|
|
+ selectedRows,
|
|
|
|
|
+ onSelectAll,
|
|
|
|
|
+ onSelectRow,
|
|
|
|
|
+ onEdit,
|
|
|
|
|
+ onDelete,
|
|
|
|
|
+}: MenuTreeTableProps) {
|
|
|
|
|
+ // 创建表格列配置
|
|
|
|
|
+ const columns = createColumns(onEdit, onDelete);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取所有菜单ID(包括子菜单)
|
|
|
|
|
+ const getAllMenuIds = (menus: SysMenu[]): string[] => {
|
|
|
|
|
+ const ids: string[] = [];
|
|
|
|
|
+ const traverse = (menuItems: SysMenu[]) => {
|
|
|
|
|
+ menuItems.forEach((menu) => {
|
|
|
|
|
+ ids.push(menu.id);
|
|
|
|
|
+ if (menu.children && menu.children.length > 0) {
|
|
|
|
|
+ traverse(menu.children);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+ traverse(menus);
|
|
|
|
|
+ return ids;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const allMenuIds = getAllMenuIds(menuList);
|
|
|
|
|
+ const isAllSelected = allMenuIds.length > 0 && allMenuIds.every(id => selectedRows.includes(id));
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Table>
|
|
|
|
|
+ <Table.Head>
|
|
|
|
|
+ <Table.Row>
|
|
|
|
|
+ <Table.Cell>
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="checkbox"
|
|
|
|
|
+ checked={isAllSelected}
|
|
|
|
|
+ onChange={(e) => onSelectAll(e.target.checked)}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Table.Cell>
|
|
|
|
|
+ {columns.map((col) => (
|
|
|
|
|
+ <Table.Cell key={col.accessor}>{col.header}</Table.Cell>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Table.Row>
|
|
|
|
|
+ </Table.Head>
|
|
|
|
|
+ <Table.Body>
|
|
|
|
|
+ {menuList.map((menu) => (
|
|
|
|
|
+ <MenuTreeRow
|
|
|
|
|
+ key={menu.id}
|
|
|
|
|
+ menu={menu}
|
|
|
|
|
+ level={0}
|
|
|
|
|
+ selectedRows={selectedRows}
|
|
|
|
|
+ onSelectRow={onSelectRow}
|
|
|
|
|
+ onEdit={onEdit}
|
|
|
|
|
+ onDelete={onDelete}
|
|
|
|
|
+ columns={columns}
|
|
|
|
|
+ />
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Table.Body>
|
|
|
|
|
+ </Table>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|