layout.tsx 9.1 KB

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