diff --git a/fake/system.fake.ts b/fake/system.fake.ts index a104e12..ad3f3e7 100644 --- a/fake/system.fake.ts +++ b/fake/system.fake.ts @@ -6,7 +6,6 @@ import { resultSuccess } from "./utils"; const systemMenu = [ // 系统管理 { - parentId: 0, id: system, menuType: 0, // 菜单类型(0 代表菜单、1 代表 iframe、2 代表外链、3 代表按钮) title: "common.menu.system", @@ -39,19 +38,19 @@ const systemMenu = [ parentId: system + 4, id: system + 4 + 1, menuType: 3, - title: "新增", + title: "common.add", }, { parentId: system + 4, id: system + 4 + 2, menuType: 3, - title: "编辑", + title: "common.edit", }, { parentId: system + 4, id: system + 4 + 3, menuType: 3, - title: "删除", + title: "common.delete", }, ]; @@ -85,7 +84,7 @@ export default defineFakeRoute([ list = list.filter(item => item.name.includes(body?.name ?? "") && String(item.status).includes(String(body?.status ?? "")) - && (!body.code || item.code === body.code), + && (!body?.code || item.code === body?.code), ); return resultSuccess({ list, @@ -141,4 +140,118 @@ export default defineFakeRoute([ return resultSuccess([]); }, }, + // 菜单管理 + { + url: "/menu-list", + method: "get", + response: () => { + const menuList = [ + // 系统管理 + { + parentId: "", // 上级菜单 id + id: system, // 菜单 id + menuType: 0, // 菜单类型(0 代表菜单、1 代表 iframe、2 代表外链、3 代表按钮) + title: "common.menu.system", // 菜单名称 + path: "/system", // 路由路径 + component: "/system", // 组件路径 + order: system, // 菜单顺序 + icon: "SettingOutlined", // 菜单图标 + currentActiveMenu: "", // 激活路径 + iframeLink: "", // iframe 链接 + keepAlive: true, // 是否缓存页面 + externalLink: "", // 外链地址 + hideInMenu: false, // 是否隐藏菜单 + ignoreAccess: false, // 是否忽略权限 + }, + { + parentId: system, + id: system + 1, + menuType: 0, + title: "common.menu.system.user", + path: "/system/user", // 路由路径 + component: "/system/user", // 组件路径 + order: undefined, // 菜单顺序 + icon: "UserOutlined", // 菜单图标 + currentActiveMenu: "", // 激活路径 + iframeLink: "", // iframe 链接 + keepAlive: true, // 是否缓存页面 + externalLink: "", // 外链地址 + hideInMenu: false, // 是否隐藏菜单 + ignoreAccess: false, // 是否忽略权限 + }, + { + parentId: system, + id: system + 2, + menuType: 0, + title: "common.menu.system.role", + path: "/system/role", // 路由路径 + component: "/system/role", // 组件路径 + order: undefined, // 菜单顺序 + icon: "TeamOutlined", // 菜单图标 + currentActiveMenu: "", // 激活路径 + iframeLink: "", // iframe 链接 + keepAlive: true, // 是否缓存页面 + externalLink: "", // 外链地址 + hideInMenu: false, // 是否隐藏菜单 + ignoreAccess: false, // 是否忽略权限 + }, + { + parentId: system, + id: system + 3, + menuType: 0, + title: "common.menu.system.menu", + path: "/system/menu", // 路由路径 + component: "/system/menu", // 组件路径 + order: undefined, // 菜单顺序 + icon: "MenuOutlined", // 菜单图标 + currentActiveMenu: "", // 激活路径 + iframeLink: "", // iframe 链接 + keepAlive: true, // 是否缓存页面 + externalLink: "", // 外链地址 + hideInMenu: false, // 是否隐藏菜单 + ignoreAccess: false, // 是否忽略权限 + }, + { + parentId: system, + id: system + 4, + menuType: 0, + title: "common.menu.system.dept", + path: "/system/dept", // 路由路径 + component: "/system/dept", // 组件路径 + order: undefined, // 菜单顺序 + icon: "ApartmentOutlined", // 菜单图标 + currentActiveMenu: "", // 激活路径 + iframeLink: "", // iframe 链接 + keepAlive: true, // 是否缓存页面 + externalLink: "", // 外链地址 + hideInMenu: false, // 是否隐藏菜单 + ignoreAccess: false, // 是否忽略权限 + }, + { + parentId: system + 4, + id: system + 4 + 1, + menuType: 3, + title: "common.add", + }, + { + parentId: system + 4, + id: system + 4 + 2, + menuType: 3, + title: "common.edit", + }, + { + parentId: system + 4, + id: system + 4 + 3, + menuType: 3, + title: "common.delete", + }, + ]; + return resultSuccess({ + list: menuList, + total: menuList.length, // 总条目数 + pageSize: 10, // 每页显示条目个数 + current: 1, // 当前页数 + }); + }, + }, ]); diff --git a/src/components/basic-table/constants.ts b/src/components/basic-table/constants.ts new file mode 100644 index 0000000..d69a887 --- /dev/null +++ b/src/components/basic-table/constants.ts @@ -0,0 +1,2 @@ +// 添加到 table 的 classname +export const BASIC_TABLE_ROOT_CLASS_NAME = "basic-table"; diff --git a/src/components/basic-table/index.tsx b/src/components/basic-table/index.tsx index 03e5611..5f6b65e 100644 --- a/src/components/basic-table/index.tsx +++ b/src/components/basic-table/index.tsx @@ -1,15 +1,32 @@ import type { ParamsType, ProTableProps } from "@ant-design/pro-components"; + +import { cn } from "#src/utils/cn"; + +import { LoadingOutlined } from "@ant-design/icons"; import { ProTable } from "@ant-design/pro-components"; import { useSize } from "ahooks"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; -export interface BasicTableProps extends ProTableProps { +import { BASIC_TABLE_ROOT_CLASS_NAME } from "./constants"; +import { useStyles } from "./styles"; +export interface BasicTableProps extends ProTableProps { + /** + * @description 是否填充父元素 + * @default true + */ + autoHeight?: boolean + /** + * @description 表格底部的偏移量 + * @default 0 + */ + offsetBottom?: number } -// 添加到 table 的 classname -export const ROOT_CLASS_NAME = "basic-table"; - +/** + * 当表格始终处于屏幕内时,表格高度是自适应的 + * 如果表格在屏幕内可能不可见或者不位于 main 布局标签内,请设置 autoHeight 为 false,避免表格出现抖动,参考下面的 warning 部分 + */ export function BasicTable< DataType extends Record, Params extends ParamsType = ParamsType, @@ -17,8 +34,11 @@ export function BasicTable< >( props: BasicTableProps, ) { + const classes = useStyles(); + const { autoHeight = true, offsetBottom } = props; const tableWrapperRef = useRef(null); const size = useSize(tableWrapperRef); + const [scrollY, setScrollY] = useState(autoHeight ? 0 : undefined); /** * @description 表格高度自适应 @@ -26,14 +46,22 @@ export function BasicTable< * @see https://github.com/ant-design/ant-design/issues/23974 */ useEffect(() => { - if (tableWrapperRef.current && size?.height) { + const isPaginationDisabled = props.pagination === false; + if (autoHeight && tableWrapperRef.current && size?.height) { const tableWrapperHeight = size.height; - const basicTable = tableWrapperRef.current.getElementsByClassName(ROOT_CLASS_NAME)[0]; + const basicTable = tableWrapperRef.current.getElementsByClassName(BASIC_TABLE_ROOT_CLASS_NAME)[0]; if (!basicTable) return; - // const antPagination = basicTable.getElementsByClassName("ant-pagination")[0]; + const tableWrapperRect = tableWrapperRef.current.getBoundingClientRect(); + + // 如果表格超出屏幕高度,不进行高度自适应 + if (tableWrapperRect.top > window.innerHeight) { + setScrollY(undefined); + return; + } + const tableBody = basicTable.querySelector("div.ant-table-body"); if (!tableBody) @@ -41,27 +69,59 @@ export function BasicTable< // 获取元素的边界框 const tableBodyRect = tableBody.getBoundingClientRect(); - const tableWrapperRect = tableWrapperRef.current.getBoundingClientRect(); - // const antPaginationRect = antPagination?.getBoundingClientRect?.() || 0; - // 上边距 - const distanceTop = tableBodyRect.top - tableWrapperRect.top; - // 下边距 - // const distanceBottom = tableBodyRect.bottom - antPaginationRect?.bottom; - /* pagination 的高度 24,上边距 16,main 标签的 padding-bottom 16 */ - const distanceBottom = 24 + 16 + 16; + // 表格表头的高度 + const tableHeaderHeight = tableBodyRect.top - tableWrapperRect.top; + /** + * 表格分页的高度 + * + * @warning 表格必须是 main 标签的子元素,因为 main 标签的 padding-bottom(16) 会影响表格的高度 + * pagination 的高度 24,上边距 16,main 标签的 padding-bottom 16 + * + * 无法通过获取分页器 DOM 来计算分页器距离屏幕底部高度的原因: + * 1. 分页器的 DOM 可能是 undefined,可能因为分页器是在表格渲染完成后才会渲染 + * 2. 没有设置 table body 的高度,无法确保分页器位于正确的位置,导致高度计算不准确 + * + */ + const paginationHeight = isPaginationDisabled ? 16 : 24 + 16 + 16; + const realOffsetBottom = offsetBottom || paginationHeight; - const bodyHeight = Math.max(200, tableWrapperHeight - distanceTop - distanceBottom); - tableBody.setAttribute("style", `overflow-y: scroll;height: ${bodyHeight}px;max-height: ${bodyHeight}px;`); + const bodyHeight = Math.max(400, tableWrapperHeight - tableHeaderHeight - realOffsetBottom); + if (bodyHeight - tableBodyRect.height <= 10) { + return; + } + tableBody.setAttribute("style", `overflow-y: auto;min-height: ${bodyHeight}px;max-height: ${bodyHeight}px;`); + } + }, [size, autoHeight, offsetBottom, props.pagination]); + + const getLoadingProps = () => { + if (props.loading === false) { + return false; + } + if (props.loading === true) { + return true; } - }, [size]); + return { + indicator: , + ...props.loading, + }; + }; return (
); diff --git a/src/components/basic-table/styles.ts b/src/components/basic-table/styles.ts new file mode 100644 index 0000000..9103137 --- /dev/null +++ b/src/components/basic-table/styles.ts @@ -0,0 +1,17 @@ +import { createUseStyles } from "react-jss"; + +export const useStyles = createUseStyles(({ prefixCls, isDark }) => { + return { + basicTable: { + [`& .${prefixCls}-table`]: { + [`& .${prefixCls}-table-container`]: { + [`& .${prefixCls}-table-content, & .${prefixCls}-table-body`]: { + "scrollbar-width": "thin", + "scrollbar-color": isDark ? "#909399 transparent" : "#eaeaea transparent", + "scrollbar-gutter": "stable", + }, + }, + }, + }, + }; +}); diff --git a/src/components/jss-theme-provider/index.tsx b/src/components/jss-theme-provider/index.tsx index 3203adf..096b66e 100644 --- a/src/components/jss-theme-provider/index.tsx +++ b/src/components/jss-theme-provider/index.tsx @@ -1,7 +1,9 @@ import type { ReactNode } from "react"; + import { usePreferences } from "#src/hooks"; -import { theme } from "antd"; +import { ConfigProvider, theme } from "antd"; +import { useContext } from "react"; import { ThemeProvider } from "react-jss"; /** @@ -32,11 +34,13 @@ const { useToken } = theme; * @returns {JSX.Element} 返回的JSX元素 */ export function JSSThemeProvider({ children }: JSSThemeProviderProps) { + const antdContext = useContext(ConfigProvider.ConfigContext); + const prefixCls = antdContext.getPrefixCls(); const { token } = useToken(); const { theme, isDark, isLight } = usePreferences(); return ( - + {children} ); diff --git a/src/pages/system/role/index.tsx b/src/pages/system/role/index.tsx index 68fe320..7affd7f 100644 --- a/src/pages/system/role/index.tsx +++ b/src/pages/system/role/index.tsx @@ -18,12 +18,14 @@ const constantColumns: ProColumns[] = [ dataIndex: "index", title: "角色编号", valueType: "indexBorder", + width: 80, }, { title: "角色名称", dataIndex: "name", disable: true, ellipsis: true, + width: 120, formItemProps: { rules: [ { @@ -37,6 +39,7 @@ const constantColumns: ProColumns[] = [ disable: true, title: "角色标识", dataIndex: "code", + width: 120, filters: true, onFilter: true, ellipsis: true, @@ -46,6 +49,7 @@ const constantColumns: ProColumns[] = [ title: "状态", dataIndex: "status", valueType: "select", + width: 80, render: (text, record) => { return {text}; }, @@ -66,8 +70,9 @@ const constantColumns: ProColumns[] = [ { title: "创建时间", dataIndex: "createTime", - valueType: "date", + valueType: "dateTime", search: false, + width: 180, }, ]; @@ -103,6 +108,7 @@ export default function Role() { title: t("common.action"), valueType: "option", key: "option", + width: 120, render: (text, record, _, action) => { return [ columns={columns} actionRef={actionRef} - cardBordered request={async (params) => { // console.log(sort, filter); const responseData = await roleListMutation.mutateAsync(params); @@ -158,27 +163,6 @@ export default function Role() { total: responseData.result.total, }; }} - rowKey="id" - search={{ - layout: "horizontal", - defaultColsNumber: 3, - showHiddenNum: true, - labelWidth: "auto", - }} - options={{ - fullScreen: true, - }} - form={{ - // 同步结果到 url 中 - syncToUrl: (values, type) => { - if (type === "get") { - return { - ...values, - }; - } - return values; - }, - }} pagination={{ position: ["bottomRight"], defaultPageSize: 10, @@ -187,10 +171,8 @@ export default function Role() { // showTotal={(total) => `Total ${total} items`} showTotal: total => `共 ${total} 条`, }} - dateFormatter="string" headerTitle="角色管理(仅演示,操作后不生效)" toolBarRender={() => [ -