layout.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. "use client";
  2. import dynamic from "next/dynamic";
  3. import {
  4. ChromeFilled,
  5. ExclamationCircleFilled,
  6. GithubOutlined,
  7. HomeOutlined,
  8. LogoutOutlined,
  9. MenuOutlined,
  10. QuestionCircleFilled,
  11. SearchOutlined,
  12. UserOutlined,
  13. FullscreenOutlined,
  14. FullscreenExitOutlined,
  15. } from "@ant-design/icons";
  16. import { ProConfigProvider } from "@ant-design/pro-components";
  17. import { Dropdown, Select, MenuProps, Modal, Tooltip } from "antd";
  18. import type { SelectProps } from "antd";
  19. import { deleteCookie, getCookie } from "cookies-next";
  20. import Link from "next/link";
  21. import { usePathname, useRouter } from "next/navigation";
  22. import { useEffect, useState, useRef } from "react";
  23. import { IconMap, RouteInfo, UserInfo } from "../_modules/definies";
  24. import "./styles.css";
  25. import "../globals.css";
  26. import {
  27. displayModeIsDark,
  28. fetchApi,
  29. watchDarkModeChange,
  30. } from "../_modules/func";
  31. //动态引入 ProLayout,禁用 SSR
  32. const ProLayout = dynamic(
  33. () => import("@ant-design/pro-components").then((mod) => mod.ProLayout),
  34. { ssr: false }
  35. );
  36. export default function RootLayout({
  37. children,
  38. }: {
  39. children: React.ReactNode;
  40. }) {
  41. const { push } = useRouter();
  42. const redirectToLogin = () => {
  43. push("/login");
  44. };
  45. const [isDark, setIsDark] = useState(false);
  46. useEffect(() => {
  47. const token = getCookie("token");
  48. if (!token) {
  49. redirectToLogin();
  50. return;
  51. }
  52. getProfile();
  53. setIsDark(displayModeIsDark());
  54. const unsubscribe = watchDarkModeChange((matches: boolean) => {
  55. setIsDark(matches);
  56. });
  57. document.addEventListener("click", hideSearchInput);
  58. return () => {
  59. unsubscribe();
  60. document.removeEventListener("click", hideSearchInput);
  61. };
  62. }, []);
  63. const searchRef = useRef<HTMLDivElement>(null);
  64. const hideSearchInput = (e: any) => {
  65. if (searchRef.current && !searchRef.current.contains(e.target)) {
  66. setShowSearch(false);
  67. setSearchListData([]);
  68. }
  69. };
  70. const [showSearch, setShowSearch] = useState(false);
  71. const [isLogoutShow, setIsLogoutShow] = useState(false);
  72. const [confirmLoading, setConfirmLoading] = useState(false);
  73. const onActionClick: MenuProps["onClick"] = ({ key }) => {
  74. if (key === "logout") {
  75. setIsLogoutShow(true);
  76. } else if (key === "profile") {
  77. push("/user/profile");
  78. }
  79. };
  80. const [userInfo, setUserInfo] = useState<UserInfo>({
  81. nickName: "Monrtnon",
  82. avatar: "/avatar1.jpeg",
  83. });
  84. const getProfile = async () => {
  85. const data = await fetchApi("/api/getInfo", push);
  86. if (data) {
  87. setUserInfo({
  88. nickName: data.user.nickName,
  89. avatar:
  90. data.user.avatar === ""
  91. ? data.user.sex === "1"
  92. ? "/avatar1.jpeg"
  93. : "/avatar0.jpeg"
  94. : "/api" + data.user.avatar,
  95. });
  96. }
  97. };
  98. const [menuData, setMenuData] = useState<any[]>([]);
  99. const getRoutes = async () => {
  100. const body = await fetchApi("/api/getRouters", push);
  101. const rootChildren: Array<RouteInfo> = [];
  102. const searchMenuList: any[] = [];
  103. const indexRoute: RouteInfo = {
  104. path: "/home",
  105. name: "首页",
  106. icon: <HomeOutlined />,
  107. };
  108. searchMenuList.push({ value: indexRoute.path, text: indexRoute.name });
  109. rootChildren.push(indexRoute);
  110. if (body.data?.length > 0) {
  111. body.data.forEach((menu: any) => {
  112. const route: RouteInfo = {
  113. path: menu.path,
  114. name: menu.meta.title,
  115. icon:
  116. menu.meta.icon !== null ? (
  117. IconMap[menu.meta.icon.replace(/-/g, "") as "system"]
  118. ) : (
  119. <MenuOutlined />
  120. ),
  121. };
  122. if (menu.children?.length > 0) {
  123. if (route.name != null) {
  124. getSubMenu(route, menu.children, route.name, route.path, searchMenuList);
  125. }
  126. }
  127. rootChildren.push(route);
  128. });
  129. }
  130. setMenuData(searchMenuList);
  131. return rootChildren;
  132. };
  133. const getSubMenu = (
  134. parent: RouteInfo,
  135. menuChildren: any[],
  136. parentName: string,
  137. parentPath: string,
  138. searchMenuList: any[]
  139. ) => {
  140. const routeChildren: Array<RouteInfo> = [];
  141. menuChildren.forEach((menu: any) => {
  142. const route: RouteInfo = {
  143. path: menu.path,
  144. name: menu.meta.title,
  145. icon:
  146. menu.meta.icon !== null ? (
  147. IconMap[menu.meta.icon.replace(/-/g, "") as "system"]
  148. ) : (
  149. <MenuOutlined />
  150. ),
  151. };
  152. routeChildren.push(route);
  153. if (menu.children?.length > 0) {
  154. getSubMenu(
  155. route,
  156. menu.children,
  157. parentName + " > " + route.name,
  158. parentPath + "/" + route.path,
  159. searchMenuList
  160. );
  161. } else {
  162. searchMenuList.push({
  163. text: parentName + " > " + menu.meta.title,
  164. value: parentPath + "/" + route.path,
  165. });
  166. }
  167. });
  168. parent.routes = routeChildren;
  169. };
  170. const logout = async () => {
  171. setConfirmLoading(true);
  172. const data = await fetchApi("/api/logout", push, { method: "POST" });
  173. if (data.code === 200) {
  174. deleteCookie("token");
  175. redirectToLogin();
  176. setIsLogoutShow(false);
  177. setConfirmLoading(false);
  178. }
  179. };
  180. const pathName = usePathname();
  181. const [pathname, setPathname] = useState(pathName);
  182. const [searchListData, setSearchListData] = useState<SelectProps["options"]>([]);
  183. const handleSearch = (newValue: string) => {
  184. if (!newValue) return;
  185. setSearchListData(menuData.filter((item) => item.text.includes(newValue)));
  186. };
  187. const handleSearchChange = (path: string) => {
  188. setPathname(path || "/index");
  189. push(path);
  190. };
  191. const [isFullscreen, setIsFullscreen] = useState(false);
  192. const openFullscreen = () => {
  193. document.documentElement.requestFullscreen?.();
  194. setIsFullscreen(true);
  195. };
  196. const closeFullscreen = () => {
  197. document.exitFullscreen?.();
  198. setIsFullscreen(false);
  199. };
  200. return (
  201. <ProConfigProvider dark={isDark}>
  202. <ProLayout
  203. title="MorTnon 若依"
  204. logo="https://static.dongfangzan.cn/img/mortnon.svg"
  205. menu={{ request: getRoutes }}
  206. layout="mix"
  207. splitMenus={false}
  208. defaultCollapsed={false}
  209. breakpoint={false}
  210. fixedHeader={false}
  211. location={{ pathname }}
  212. onMenuHeaderClick={(e) => console.log(e)}
  213. menuItemRender={(item, dom) => (
  214. <div onClick={() => setPathname(item.path || "/index")}>
  215. <Link href={item.path ?? ""}>{dom}</Link>
  216. </div>
  217. )}
  218. subMenuItemRender={(item, dom) => dom}
  219. avatarProps={{
  220. src: userInfo.avatar,
  221. size: "small",
  222. title: userInfo.nickName,
  223. render: (_, dom) => (
  224. <Dropdown
  225. menu={{
  226. items: [
  227. { key: "profile", icon: <UserOutlined />, label: "个人中心" },
  228. { type: "divider" },
  229. { key: "logout", icon: <LogoutOutlined />, label: "退出登录" },
  230. ],
  231. onClick: onActionClick,
  232. }}
  233. >
  234. {dom}
  235. </Dropdown>
  236. ),
  237. }}
  238. actionsRender={(props) => {
  239. if (props.isMobile) return [];
  240. return [
  241. <div
  242. key="search"
  243. style={{ height: "100%", display: "flex", alignItems: "center" }}
  244. ref={searchRef}
  245. >
  246. <SearchOutlined
  247. style={{ color: "var(--ant-primary-color)", marginRight: showSearch ? 8 : 0 }}
  248. onClick={() => setShowSearch(!showSearch)}
  249. />
  250. {showSearch && (
  251. <Select
  252. showSearch
  253. autoFocus
  254. style={{ borderRadius: 4, marginInlineEnd: 12, width: 300 }}
  255. suffixIcon={null}
  256. placeholder="搜索菜单"
  257. variant="borderless"
  258. filterOption={false}
  259. notFoundContent={null}
  260. onSearch={handleSearch}
  261. onChange={handleSearchChange}
  262. options={searchListData?.map((d) => ({ value: d.value, label: d.text }))}
  263. />
  264. )}
  265. </div>,
  266. <Link key="github" href="https://github.com/mortise-and-tenon/RuoYi-React-Pro" target="_blank">
  267. <Tooltip title="Github 源码仓库">
  268. <GithubOutlined style={{ color: "gray" }} />
  269. </Tooltip>
  270. </Link>,
  271. <Link key="question" href="https://doc.ruoyi.vip/ruoyi-vue/" target="_blank">
  272. <Tooltip title="RuoYi 文档">
  273. <QuestionCircleFilled style={{ color: "gray" }} />
  274. </Tooltip>
  275. </Link>,
  276. isFullscreen ? (
  277. <FullscreenExitOutlined key="exit" onClick={closeFullscreen} />
  278. ) : (
  279. <FullscreenOutlined key="full" onClick={openFullscreen} />
  280. ),
  281. ];
  282. }}
  283. menuFooterRender={(props) =>
  284. props?.collapsed ? undefined : (
  285. <div style={{ textAlign: "center", paddingBlockStart: 12 }}>
  286. <div>©{new Date().getFullYear()} Mortnon.</div>
  287. </div>
  288. )
  289. }
  290. >
  291. <Modal
  292. title={<><ExclamationCircleFilled style={{ color: "#faad14" }} /> 提示</>}
  293. open={isLogoutShow}
  294. onOk={logout}
  295. onCancel={() => setIsLogoutShow(false)}
  296. confirmLoading={confirmLoading}
  297. >
  298. 确定注销并退出系统吗?
  299. </Modal>
  300. {children}
  301. </ProLayout>
  302. </ProConfigProvider>
  303. );
  304. }