layout.tsx 9.7 KB

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