فهرست منبع

feat: 初始化 ruoyi-react项目

- 添加项目基本结构和配置文件
- 配置 ESLint 和 TailwindCSS
- 设置 Next.js 和 React
- 添加Ant Design相关依赖
- 配置项目忽略文件和目录
nahida 9 ماه پیش
والد
کامیت
c106d44bbb
72فایلهای تغییر یافته به همراه16363 افزوده شده و 1422 حذف شده
  1. 5 0
      .idea/.gitignore
  2. 10 0
      .idea/UniappTool.xml
  3. 6 0
      .idea/inspectionProfiles/Project_Default.xml
  4. 8 0
      .idea/modules.xml
  5. 12 0
      .idea/ruoyi-react.iml
  6. 6 0
      .idea/vcs.xml
  7. 58 0
      app/(business)/home/page.tsx
  8. 327 0
      app/(business)/layout.tsx
  9. 287 0
      app/(business)/monitor/cache/page.tsx
  10. 345 0
      app/(business)/monitor/cacheList/page.tsx
  11. 19 0
      app/(business)/monitor/druid/page.tsx
  12. 542 0
      app/(business)/monitor/job-log/index/[jobid]/page.tsx
  13. 1103 0
      app/(business)/monitor/job/page.tsx
  14. 49 0
      app/(business)/monitor/job/styles.scss
  15. 244 0
      app/(business)/monitor/online/page.tsx
  16. 289 0
      app/(business)/monitor/server/page.tsx
  17. 9 0
      app/(business)/styles.css
  18. 686 0
      app/(business)/system/config/page.tsx
  19. 718 0
      app/(business)/system/dept/page.tsx
  20. 818 0
      app/(business)/system/dict-data/index/[dictid]/page.tsx
  21. 667 0
      app/(business)/system/dict/page.tsx
  22. 461 0
      app/(business)/system/log/logininfor/page.tsx
  23. 603 0
      app/(business)/system/log/operlog/page.tsx
  24. 887 0
      app/(business)/system/menu/page.tsx
  25. 616 0
      app/(business)/system/notice/page.tsx
  26. 634 0
      app/(business)/system/post/page.tsx
  27. 616 0
      app/(business)/system/role/auth/[roleid]/page.tsx
  28. 998 0
      app/(business)/system/role/page.tsx
  29. 184 0
      app/(business)/system/user/auth/[userid]/page.tsx
  30. 1350 0
      app/(business)/system/user/page.tsx
  31. 12 0
      app/(business)/test/test2/page.tsx
  32. 11 0
      app/(business)/test1/page.tsx
  33. 12 0
      app/(business)/tool/build/page.tsx
  34. 12 0
      app/(business)/tool/gen/page.tsx
  35. 19 0
      app/(business)/tool/swagger/page.tsx
  36. 409 0
      app/(business)/user/profile/page.tsx
  37. 356 0
      app/_modules/definies.tsx
  38. 135 0
      app/_modules/func.tsx
  39. BIN
      app/favicon.ico
  40. 1 0
      app/globals.css
  41. 27 0
      app/layout.tsx
  42. 339 0
      app/login/page.tsx
  43. 348 0
      app/normalize.css
  44. 3 0
      app/page.module.css
  45. 57 0
      app/page.tsx
  46. 5 0
      eslint.config.mjs
  47. 30 4
      next.config.ts
  48. 1905 1248
      package-lock.json
  49. 23 6
      package.json
  50. BIN
      public/1.png
  51. BIN
      public/2.png
  52. BIN
      public/3.png
  53. BIN
      public/4.png
  54. BIN
      public/5.png
  55. BIN
      public/6.png
  56. BIN
      public/7.png
  57. 43 0
      public/antd.svg
  58. 58 0
      public/antdpro.svg
  59. BIN
      public/avatar0.jpeg
  60. BIN
      public/avatar1.jpeg
  61. BIN
      public/bg-dark.jpg
  62. BIN
      public/bg.jpg
  63. BIN
      public/bg2.jpg
  64. BIN
      public/bg3.jpg
  65. BIN
      public/bg4.jpg
  66. BIN
      public/fontawesome.ico
  67. BIN
      public/nextjs.ico
  68. BIN
      src/app/favicon.ico
  69. 0 26
      src/app/globals.css
  70. 0 34
      src/app/layout.tsx
  71. 0 103
      src/app/page.tsx
  72. 1 1
      tsconfig.json

+ 5 - 0
.idea/.gitignore

@@ -0,0 +1,5 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/

+ 10 - 0
.idea/UniappTool.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="cn.fjdmy.uniapp.UniappProjectDataService">
+    <option name="generalBasePath" value="$PROJECT_DIR$" />
+    <option name="manifestPath" value="$PROJECT_DIR$/manifest.json" />
+    <option name="pagesPath" value="$PROJECT_DIR$/pages.json" />
+    <option name="scanNum" value="1" />
+    <option name="type" value="store" />
+  </component>
+</project>

+ 6 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
+  </profile>
+</component>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/ruoyi-react.iml" filepath="$PROJECT_DIR$/.idea/ruoyi-react.iml" />
+    </modules>
+  </component>
+</project>

+ 12 - 0
.idea/ruoyi-react.iml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.tmp" />
+      <excludeFolder url="file://$MODULE_DIR$/temp" />
+      <excludeFolder url="file://$MODULE_DIR$/tmp" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 58 - 0
app/(business)/home/page.tsx

@@ -0,0 +1,58 @@
+"use client";
+
+import React from "react";
+
+const subsystems = [
+  {
+    name: "城市生命线驾驶舱",
+    img: "/1.png",
+  },
+  {
+    name: "生命线预警联动处置平台",
+    img: "/2.png",
+  },
+  {
+    name: "生命线物联网平台",
+    img: "/3.png",
+  },
+  {
+    name: "燃气管网安全运行监测子系统",
+    img: "/4.png",
+  },
+  {
+    name: "供水管网安全运行监测子系统",
+    img: "/5.png",
+  },
+  {
+    name: "排水管网安全运行监测子系统",
+    img: "/6.png",
+  },
+  {
+    name: "窨井盖安全运行监测子系统",
+    img: "/7.png",
+  },
+];
+
+export default function CityLifelinePortal() {
+  return (
+    <div className="min-h-full w-full bg-slate-50 p-10 font-sans flex flex-col">
+      <div className="flex-1 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-10">
+        {subsystems.map((s) => (
+          <div
+            key={s.name}
+            className="bg-white rounded-3xl shadow-xl cursor-pointer hover:shadow-2xl hover:bg-sky-50 transition transform hover:scale-110 flex flex-col overflow-hidden border-4 border-transparent hover:border-sky-200"
+            onClick={() => alert(`进入 ${s.name}`)}
+          >
+            <div className="w-full h-56 overflow-hidden">
+              <img src={s.img} alt={s.name} className="w-full h-full object-cover" />
+            </div>
+            <div className="p-6 flex flex-col items-center justify-center text-center">
+              <div className="text-xl font-semibold text-slate-800 mb-3">{s.name}</div>
+              <div className="text-slate-500 text-base">点击进入</div>
+            </div>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}

+ 327 - 0
app/(business)/layout.tsx

@@ -0,0 +1,327 @@
+"use client";
+
+import dynamic from "next/dynamic";
+import {
+  ChromeFilled,
+  ExclamationCircleFilled,
+  GithubOutlined,
+  HomeOutlined,
+  LogoutOutlined,
+  MenuOutlined,
+  QuestionCircleFilled,
+  SearchOutlined,
+  UserOutlined,
+  FullscreenOutlined,
+  FullscreenExitOutlined,
+} from "@ant-design/icons";
+import { ProConfigProvider } from "@ant-design/pro-components";
+import { Dropdown, Select, MenuProps, Modal, Tooltip } from "antd";
+import type { SelectProps } from "antd";
+import { deleteCookie, getCookie } from "cookies-next";
+import Link from "next/link";
+import { usePathname, useRouter } from "next/navigation";
+import { useEffect, useState, useRef } from "react";
+import { IconMap, RouteInfo, UserInfo } from "../_modules/definies";
+import "./styles.css";
+import "../globals.css";
+import {
+  displayModeIsDark,
+  fetchApi,
+  watchDarkModeChange,
+} from "../_modules/func";
+
+//动态引入 ProLayout,禁用 SSR
+const ProLayout = dynamic(
+  () => import("@ant-design/pro-components").then((mod) => mod.ProLayout),
+  { ssr: false }
+);
+
+export default function RootLayout({
+                                     children,
+                                   }: {
+  children: React.ReactNode;
+}) {
+  const { push } = useRouter();
+
+  const redirectToLogin = () => {
+    push("/login");
+  };
+
+  const [isDark, setIsDark] = useState(false);
+
+  useEffect(() => {
+    const token = getCookie("token");
+    if (!token) {
+      redirectToLogin();
+      return;
+    }
+    getProfile();
+
+    setIsDark(displayModeIsDark());
+    const unsubscribe = watchDarkModeChange((matches: boolean) => {
+      setIsDark(matches);
+    });
+
+    document.addEventListener("click", hideSearchInput);
+    return () => {
+      unsubscribe();
+      document.removeEventListener("click", hideSearchInput);
+    };
+  }, []);
+
+  const searchRef = useRef<HTMLDivElement>(null);
+  const hideSearchInput = (e: any) => {
+    if (searchRef.current && !searchRef.current.contains(e.target)) {
+      setShowSearch(false);
+      setSearchListData([]);
+    }
+  };
+
+  const [showSearch, setShowSearch] = useState(false);
+  const [isLogoutShow, setIsLogoutShow] = useState(false);
+  const [confirmLoading, setConfirmLoading] = useState(false);
+
+  const onActionClick: MenuProps["onClick"] = ({ key }) => {
+    if (key === "logout") {
+      setIsLogoutShow(true);
+    } else if (key === "profile") {
+      push("/user/profile");
+    }
+  };
+
+  const [userInfo, setUserInfo] = useState<UserInfo>({
+    nickName: "Monrtnon",
+    avatar: "/avatar1.jpeg",
+  });
+
+  const getProfile = async () => {
+    const data = await fetchApi("/api/getInfo", push);
+    if (data) {
+      setUserInfo({
+        nickName: data.user.nickName,
+        avatar:
+          data.user.avatar === ""
+            ? data.user.sex === "1"
+              ? "/avatar1.jpeg"
+              : "/avatar0.jpeg"
+            : "/api" + data.user.avatar,
+      });
+    }
+  };
+
+  const [menuData, setMenuData] = useState<any[]>([]);
+  const getRoutes = async () => {
+    const body = await fetchApi("/api/getRouters", push);
+    const rootChildren: Array<RouteInfo> = [];
+    const searchMenuList: any[] = [];
+
+    const indexRoute: RouteInfo = {
+      path: "/home",
+      name: "首页",
+      icon: <HomeOutlined />,
+    };
+    searchMenuList.push({ value: indexRoute.path, text: indexRoute.name });
+    rootChildren.push(indexRoute);
+
+    if (body.data?.length > 0) {
+      body.data.forEach((menu: any) => {
+        const route: RouteInfo = {
+          path: menu.path,
+          name: menu.meta.title,
+          icon:
+            menu.meta.icon !== null ? (
+              IconMap[menu.meta.icon.replace(/-/g, "") as "system"]
+            ) : (
+              <MenuOutlined />
+            ),
+        };
+        if (menu.children?.length > 0) {
+          if (route.name != null) {
+            getSubMenu(route, menu.children, route.name, route.path, searchMenuList);
+          }
+        }
+        rootChildren.push(route);
+      });
+    }
+    setMenuData(searchMenuList);
+    return rootChildren;
+  };
+
+  const getSubMenu = (
+    parent: RouteInfo,
+    menuChildren: any[],
+    parentName: string,
+    parentPath: string,
+    searchMenuList: any[]
+  ) => {
+    const routeChildren: Array<RouteInfo> = [];
+    menuChildren.forEach((menu: any) => {
+      const route: RouteInfo = {
+        path: menu.path,
+        name: menu.meta.title,
+        icon:
+          menu.meta.icon !== null ? (
+            IconMap[menu.meta.icon.replace(/-/g, "") as "system"]
+          ) : (
+            <MenuOutlined />
+          ),
+      };
+      routeChildren.push(route);
+
+      if (menu.children?.length > 0) {
+        getSubMenu(
+          route,
+          menu.children,
+          parentName + " > " + route.name,
+          parentPath + "/" + route.path,
+          searchMenuList
+        );
+      } else {
+        searchMenuList.push({
+          text: parentName + " > " + menu.meta.title,
+          value: parentPath + "/" + route.path,
+        });
+      }
+    });
+    parent.routes = routeChildren;
+  };
+
+  const logout = async () => {
+    setConfirmLoading(true);
+    const data = await fetchApi("/api/logout", push, { method: "POST" });
+    if (data.code === 200) {
+      deleteCookie("token");
+      redirectToLogin();
+      setIsLogoutShow(false);
+      setConfirmLoading(false);
+    }
+  };
+
+  const pathName = usePathname();
+  const [pathname, setPathname] = useState(pathName);
+
+  const [searchListData, setSearchListData] = useState<SelectProps["options"]>([]);
+  const handleSearch = (newValue: string) => {
+    if (!newValue) return;
+    setSearchListData(menuData.filter((item) => item.text.includes(newValue)));
+  };
+  const handleSearchChange = (path: string) => {
+    setPathname(path || "/index");
+    push(path);
+  };
+
+  const [isFullscreen, setIsFullscreen] = useState(false);
+  const openFullscreen = () => {
+    document.documentElement.requestFullscreen?.();
+    setIsFullscreen(true);
+  };
+  const closeFullscreen = () => {
+    document.exitFullscreen?.();
+    setIsFullscreen(false);
+  };
+
+  return (
+    <ProConfigProvider dark={isDark}>
+      <ProLayout
+        title="MorTnon 若依"
+        logo="https://static.dongfangzan.cn/img/mortnon.svg"
+        menu={{ request: getRoutes }}
+        layout="mix"
+        splitMenus={false}
+        defaultCollapsed={false}
+        breakpoint={false}
+        fixedHeader={false}
+        location={{ pathname }}
+        onMenuHeaderClick={(e) => console.log(e)}
+        menuItemRender={(item, dom) => (
+          <div onClick={() => setPathname(item.path || "/index")}>
+            <Link href={item.path ?? ""}>{dom}</Link>
+          </div>
+        )}
+        subMenuItemRender={(item, dom) => dom}
+        avatarProps={{
+          src: userInfo.avatar,
+          size: "small",
+          title: userInfo.nickName,
+          render: (_, dom) => (
+            <Dropdown
+              menu={{
+                items: [
+                  { key: "profile", icon: <UserOutlined />, label: "个人中心" },
+                  { type: "divider" },
+                  { key: "logout", icon: <LogoutOutlined />, label: "退出登录" },
+                ],
+                onClick: onActionClick,
+              }}
+            >
+              {dom}
+            </Dropdown>
+          ),
+        }}
+        actionsRender={(props) => {
+          if (props.isMobile) return [];
+          return [
+            <div
+              key="search"
+              style={{ height: "100%", display: "flex", alignItems: "center" }}
+              ref={searchRef}
+            >
+              <SearchOutlined
+                style={{ color: "var(--ant-primary-color)", marginRight: showSearch ? 8 : 0 }}
+                onClick={() => setShowSearch(!showSearch)}
+              />
+              {showSearch && (
+                <Select
+                  showSearch
+                  autoFocus
+                  style={{ borderRadius: 4, marginInlineEnd: 12, width: 300 }}
+                  suffixIcon={null}
+                  placeholder="搜索菜单"
+                  variant="borderless"
+                  filterOption={false}
+                  notFoundContent={null}
+                  onSearch={handleSearch}
+                  onChange={handleSearchChange}
+                  options={searchListData?.map((d) => ({ value: d.value, label: d.text }))}
+                />
+              )}
+            </div>,
+            <Link key="github" href="https://github.com/mortise-and-tenon/RuoYi-React-Pro" target="_blank">
+              <Tooltip title="Github 源码仓库">
+                <GithubOutlined style={{ color: "gray" }} />
+              </Tooltip>
+            </Link>,
+            <Link key="question" href="https://doc.ruoyi.vip/ruoyi-vue/" target="_blank">
+              <Tooltip title="RuoYi 文档">
+                <QuestionCircleFilled style={{ color: "gray" }} />
+              </Tooltip>
+            </Link>,
+            isFullscreen ? (
+              <FullscreenExitOutlined key="exit" onClick={closeFullscreen} />
+            ) : (
+              <FullscreenOutlined key="full" onClick={openFullscreen} />
+            ),
+          ];
+        }}
+        menuFooterRender={(props) =>
+          props?.collapsed ? undefined : (
+            <div style={{ textAlign: "center", paddingBlockStart: 12 }}>
+              <div>©{new Date().getFullYear()} Mortnon.</div>
+            </div>
+          )
+        }
+      >
+        <Modal
+          title={<><ExclamationCircleFilled style={{ color: "#faad14" }} /> 提示</>}
+          open={isLogoutShow}
+          onOk={logout}
+          onCancel={() => setIsLogoutShow(false)}
+          confirmLoading={confirmLoading}
+        >
+          确定注销并退出系统吗?
+        </Modal>
+        {children}
+      </ProLayout>
+    </ProConfigProvider>
+  );
+}

+ 287 - 0
app/(business)/monitor/cache/page.tsx

@@ -0,0 +1,287 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { useRouter } from "next/navigation";
+import { PageContainer, ProCard } from "@ant-design/pro-components";
+import { fetchApi } from "@/app/_modules/func";
+
+import {
+  faServer,
+  faChartPie,
+  faGaugeHigh,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Statistic, Col, Row, Flex } from "antd";
+
+import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
+import { Doughnut } from "react-chartjs-2";
+
+import CountUp from "react-countup";
+
+import ReactSpeedometer from "react-d3-speedometer";
+
+ChartJS.register(ArcElement, Tooltip, Legend);
+
+//查询API
+const queryAPI = "/api/monitor/cache";
+
+//图表相关定义
+type ChartDataSet = {
+  label: string;
+  data: string[];
+  backgroundColor: string[];
+};
+
+type ChartDataType = {
+  labels: string[];
+  datasets: Array<ChartDataSet>;
+};
+
+export default function Cache() {
+  const { push } = useRouter();
+  const [data, setData] = useState(undefined as any);
+  const [commandChartData, setCommandChartData] = useState<ChartDataType>(
+    {} as ChartDataType
+  );
+
+  //查询数据
+  const queryData = async () => {
+    const body = await fetchApi(queryAPI, push);
+    if (body !== undefined) {
+      if (body.code == 200) {
+        setData(body.data);
+        parseCommandChart(body.data.commandStats);
+      }
+    }
+  };
+
+  //处理命令图表数据
+  const parseCommandChart = (commandStats: any[]) => {
+    const labels: Array<string> = new Array<string>();
+    const datas: Array<string> = new Array<string>();
+
+    commandStats.forEach((item) => {
+      labels.push(item.name);
+      datas.push(item.value);
+    });
+
+    const commandSet: ChartDataSet = {
+      label: "数量",
+      data: datas,
+      backgroundColor: [
+        "#66BB6A",
+        "#FFA726",
+        "#42A5F5",
+        "#EC407A",
+        "#78909C",
+        "#FF7043",
+        "#26A69A",
+        "#9575CD",
+        "#FFB74D",
+      ],
+    };
+
+    const commandData: ChartDataType = {
+      labels: labels,
+      datasets: [commandSet],
+    };
+
+    setCommandChartData(commandData);
+  };
+
+  const options = {
+    plugins: {
+      tooltip: {
+        callbacks: {
+          label: function (context: any) {
+            let label = context.dataset.label || "";
+            if (label) {
+              label += ": ";
+            }
+
+            label +=
+              context.parsed +
+              "(" +
+              Math.round(
+                (context.parsed * 100) /
+                  context.dataset.data.reduce(
+                    (a: string, b: string) => parseInt(a) + parseInt(b),
+                    0
+                  )
+              ) +
+              "%)";
+
+            return label;
+          },
+        },
+      },
+    },
+  };
+
+  const speedParentRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    queryData();
+    window.addEventListener("resize", handleResize);
+    setDaynamicWidth();
+    return () => {
+      window.removeEventListener("resize", handleResize);
+    };
+  }, [speedParentRef]);
+
+  //数字展示的动态效果
+  const formatter = (value: any) => (
+    <CountUp end={parseInt(value)} separator="," />
+  );
+
+  //超小屏的边界值
+  const xsScreenWidth = 768;
+  //界面两个ProCard横向布局空白空间宽度
+  const spaceTotalWidth = 212;
+  //侧边栏展开占据的宽度
+  const siderExpand = 214;
+  //计算初始的速度表宽度
+  const setDaynamicWidth = () => {
+    const screenWidth = window.innerWidth;
+    if (screenWidth < xsScreenWidth) {
+      setParentWidth((screenWidth - spaceTotalWidth) / 2);
+    } else if (screenWidth >= xsScreenWidth) {
+      setParentWidth((screenWidth - spaceTotalWidth - siderExpand) / 2);
+    }
+  };
+
+  //速度表绘制的宽度值
+  const [speedWidth, setParentWidth] = useState(0);
+
+  //处理屏幕变化时,速度表宽度动态变化
+  const handleResize = () => {
+    if (speedParentRef.current) {
+      setParentWidth(speedParentRef.current.clientWidth);
+    } else {
+      console.log("no current");
+    }
+  };
+
+  return (
+    <PageContainer title={false}>
+      {data !== undefined && (
+        <>
+          <ProCard
+            title={
+              <>
+                <FontAwesomeIcon icon={faServer} />
+                <span style={{ marginLeft: 6 }}>基本信息</span>
+              </>
+            }
+            direction="row"
+            headerBordered
+            bordered
+            hoverable
+          >
+            <ProCard>
+              <Statistic title="Redis版本" value={data.info.redis_version} />
+              <Statistic
+                title="运行模式"
+                value={data.info.redis_mode == "standalone" ? "单机" : "集群"}
+              />
+              <Statistic
+                title="端口"
+                value={data.info.tcp_port}
+                groupSeparator=""
+              />
+              <Statistic
+                title="客户端数"
+                value={data.info.connected_clients}
+                formatter={formatter}
+              />
+            </ProCard>
+            <ProCard>
+              <Statistic
+                title="运行天数"
+                value={data.info.uptime_in_days}
+                formatter={formatter}
+              />
+              <Statistic title="使用内存" value={data.info.used_memory_human} />
+              <Statistic
+                title="使用CPU"
+                value={data.info.used_cpu_user_children}
+                precision={2}
+              />
+              <Statistic title="内存配置" value={data.info.maxmemory_human} />
+            </ProCard>
+            <ProCard>
+              <Statistic
+                title="AOF是否开启"
+                value={data.info.aof_enabled == "0" ? "否" : "是"}
+              />
+              <Statistic
+                title="RDB是否成功"
+                value={data.info.rdb_last_bgsave_status}
+              />
+              <Statistic
+                title="Key数量"
+                value={data.dbSize}
+                formatter={formatter}
+              />
+              <Statistic
+                title="网络入口/出口"
+                value={`${data.info.instantaneous_input_kbps}kps/${data.info.instantaneous_output_kbps}kps`}
+              />
+            </ProCard>
+          </ProCard>
+          <Row gutter={16} style={{ marginTop: 16 }}>
+            <Col span={12}>
+              <ProCard
+                title={
+                  <>
+                    <FontAwesomeIcon icon={faChartPie} />
+                    <span style={{ marginLeft: 6 }}>命令统计</span>
+                  </>
+                }
+                style={{ height: "100%" }}
+                headerBordered
+                bordered
+                hoverable
+              >
+                <Flex justify="center">
+                  <Doughnut data={commandChartData} options={options} />
+                </Flex>
+              </ProCard>
+            </Col>
+            <Col span={12}>
+              <ProCard
+                title={
+                  <>
+                    <FontAwesomeIcon icon={faGaugeHigh} />
+                    <span style={{ marginLeft: 6 }}>内存信息</span>
+                  </>
+                }
+                style={{ height: "100%" }}
+                headerBordered
+                bordered
+                hoverable
+              >
+                <Flex justify="center" ref={speedParentRef}>
+                  <ReactSpeedometer
+                    key={speedWidth}
+                    width={speedWidth}
+                    currentValueText={`内存消耗:${data.info.used_memory_human}`}
+                    value={parseFloat(data.info.used_memory_human)}
+                    needleColor="#1677ff"
+                    segmentColors={[
+                      "#82C182",
+                      "#A3D972",
+                      "#F5D061",
+                      "#FF9933",
+                      "#FF6666",
+                    ]}
+                  />
+                </Flex>
+              </ProCard>
+            </Col>
+          </Row>
+        </>
+      )}
+    </PageContainer>
+  );
+}

+ 345 - 0
app/(business)/monitor/cacheList/page.tsx

@@ -0,0 +1,345 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { ReloadOutlined, ClearOutlined } from "@ant-design/icons";
+import {
+  Row,
+  Col,
+  Tooltip,
+  Table,
+  Button,
+  Form,
+  Input,
+  Flex,
+  message,
+} from "antd";
+import { DeleteOutlined } from "@ant-design/icons";
+import { PageContainer, ProCard } from "@ant-design/pro-components";
+import {
+  faFloppyDisk,
+  faKey,
+  faFileLines,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { fetchApi } from "@/app/_modules/func";
+
+const { TextArea } = Input;
+
+//查询缓存API
+const queryCacheAPI = "/api/monitor/cache/getNames";
+//删除指定的缓存API
+const deleteCacheAPI = "/api/monitor/cache/clearCacheName";
+//查询缓存Key API
+const queryCacheKeyAPI = "/api/monitor/cache/getKeys";
+//查询缓存值API
+const queryValueAPI = "/api/monitor/cache/getValue";
+//删除指定的Key API
+const deleteKeyAPI = "/api/monitor/cache/clearCacheKey";
+//清空所有缓存API
+const clearAPI = "/api/monitor/cache/clearCacheAll";
+
+export default function CacheList() {
+  const { push } = useRouter();
+
+  //缓存列表加载状态
+  const [isCacheLoading, setIsCacheLoading] = useState(false);
+
+  //缓存数据
+  const [cacheData, setCacheData] = useState([]);
+
+  //查询缓存
+  const queryCache = async () => {
+    setIsCacheLoading(true);
+    const body = await fetchApi(queryCacheAPI, push);
+    if (body !== undefined) {
+      if (body.code == 200) {
+        setCacheData(body.data);
+      }
+    }
+    setIsCacheLoading(false);
+  };
+
+  useEffect(() => {
+    queryCache();
+  }, []);
+
+  //缓存列定义
+  const cacheColumns = [
+    {
+      title: "序号",
+      key: "index",
+      width: "60px",
+      render: (text: any, record: any, index: number) => `${index + 1}`,
+    },
+    {
+      title: "缓存名称",
+      dataIndex: "cacheName",
+      ellipsis: true,
+    },
+    {
+      title: "备注",
+      dataIndex: "remark",
+    },
+    {
+      title: "操作",
+      key: "action",
+      render: (text: any, record: any) => [
+        <Button
+          key="deleteBtn"
+          type="link"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => deleteCache(record.cacheName)}
+        ></Button>,
+      ],
+    },
+  ];
+
+  //缓存列表行点击
+  const cacheRowClick = (record: any, index: any) => {
+    return {
+      onMouseDown: (event: any) => {
+        setSelectedCache(record.cacheName);
+        queryKeys(record.cacheName);
+      },
+    };
+  };
+
+  //删除指定的缓存
+  const deleteCache = async (cacheName: string) => {
+    const body = await fetchApi(`${deleteCacheAPI}/${cacheName}}`, push, {
+      method: "DELETE",
+    });
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(`清理缓存[${cacheName}]成功`);
+        queryCache();
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //选中的缓存
+  const [selectedCache, setSelectedCache] = useState("");
+
+  //key列表加载状态
+  const [isKeyLoading, setIsKeyLoading] = useState(false);
+
+  //key 数据
+  const [keyData, setKeyData] = useState([]);
+
+  //查询缓存对应的key
+  const queryKeys = async (cacheName?: string) => {
+    if (cacheName === undefined) {
+      if (selectedCache === "") {
+        return;
+      }
+      cacheName = selectedCache;
+    }
+
+    setIsKeyLoading(true);
+    const body = await fetchApi(`${queryCacheKeyAPI}/${cacheName}`, push);
+    if (body !== undefined) {
+      if (body.code == 200) {
+        setKeyData(body.data);
+      }
+    }
+
+    setIsKeyLoading(false);
+  };
+
+  //key列定义
+  const keyColumns = [
+    {
+      title: "序号",
+      key: "index",
+      width: "60px",
+      render: (text: any, record: any, index: number) => `${index + 1}`,
+    },
+    {
+      title: "缓存键名",
+      key: "key",
+      ellipsis: true,
+      render: (text: any, record: string) => record.split(":")[1],
+    },
+    {
+      title: "操作",
+      key: "action",
+      render: (text: any, record: any) => [
+        <Button
+          key="deleteBtn"
+          type="link"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => deleteKey(record)}
+        ></Button>,
+      ],
+    },
+  ];
+
+  //key列表行点击
+  const keyRowClick = (record: any, index: any) => {
+    return {
+      onClick: (event: any) => {
+        queryValue(record);
+      },
+    };
+  };
+
+  //删除指定的key
+  const deleteKey = async (key: string) => {
+    const body = await fetchApi(`${deleteKeyAPI}/${key}`, push, {
+      method: "DELETE",
+    });
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(`清理缓存键名[${key}]成功`);
+        queryKeys();
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //缓存值展示表单
+  const [valueForm] = Form.useForm();
+
+  //查询值
+  const queryValue = async (key: any) => {
+    const body = await fetchApi(
+      `${queryValueAPI}/${selectedCache}/${key}`,
+      push
+    );
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        valueForm?.setFieldsValue({
+          cacheName: body.data.cacheName,
+          cacheKey: body.data.cacheKey,
+          cacheValue: body.data.cacheValue,
+        });
+      }
+    }
+  };
+
+  //清空全部缓存
+  const clearCache = async () => {
+    const body = await fetchApi(clearAPI, push, {
+      method: "DELETE",
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("清空全部缓存成功");
+      } else {
+        message.error(body.msg);
+      }
+    } else {
+      message.error("清空全部缓存异常");
+    }
+  };
+
+  return (
+    <PageContainer title={false}>
+      <Flex justify="flex-end" style={{ marginBottom: 16 }}>
+        <Tooltip title="清空全部缓存">
+          <Button
+            icon={<ClearOutlined />}
+            type="primary"
+            onClick={clearCache}
+          />
+        </Tooltip>
+      </Flex>
+      <Row gutter={8}>
+        <Col span={8}>
+          <ProCard
+            title={
+              <>
+                <FontAwesomeIcon icon={faFloppyDisk} />
+                <span style={{ marginLeft: 6 }}>缓存列表</span>
+              </>
+            }
+            extra={
+              <Tooltip title="刷新">
+                <a onClick={queryCache}>
+                  <ReloadOutlined />
+                </a>
+              </Tooltip>
+            }
+            style={{ height: "100%" }}
+            headerBordered
+            bordered
+            hoverable
+          >
+            <div style={{ overflowX: "auto" }}>
+              <Table
+                dataSource={cacheData}
+                columns={cacheColumns}
+                loading={isCacheLoading}
+                onRow={cacheRowClick}
+              />
+            </div>
+          </ProCard>
+        </Col>
+        <Col span={8}>
+          <ProCard
+            title={
+              <>
+                <FontAwesomeIcon icon={faKey} />
+                <span style={{ marginLeft: 6 }}>键名列表</span>
+              </>
+            }
+            extra={
+              <Tooltip title="刷新">
+                <a onClick={() => queryKeys()}>
+                  <ReloadOutlined />
+                </a>
+              </Tooltip>
+            }
+            style={{ height: "100%" }}
+            headerBordered
+            bordered
+            hoverable
+          >
+            <div style={{ overflowX: "auto" }}>
+              <Table
+                dataSource={keyData}
+                columns={keyColumns}
+                loading={isKeyLoading}
+                onRow={keyRowClick}
+              />
+            </div>
+          </ProCard>
+        </Col>
+        <Col span={8}>
+          <ProCard
+            title={
+              <>
+                <FontAwesomeIcon icon={faFileLines} />
+                <span style={{ marginLeft: 6 }}>缓存内容</span>
+              </>
+            }
+            style={{ height: "100%" }}
+            headerBordered
+            bordered
+            hoverable
+          >
+            <Form layout="vertical" form={valueForm}>
+              <Form.Item label="缓存名称" name="cacheName">
+                <Input readOnly />
+              </Form.Item>
+              <Form.Item label="缓存键名" name="cacheKey">
+                <Input readOnly />
+              </Form.Item>
+              <Form.Item label="缓存内容" name="cacheValue">
+                <TextArea readOnly rows={8} />
+              </Form.Item>
+            </Form>
+          </ProCard>
+        </Col>
+      </Row>
+    </PageContainer>
+  );
+}

+ 19 - 0
app/(business)/monitor/druid/page.tsx

@@ -0,0 +1,19 @@
+"use client";
+
+import { PageContainer } from "@ant-design/pro-components";
+
+export default function Druid() {
+
+  return (
+    <PageContainer title={false}>
+      <div style={{ height: "100vh" }}>
+        <iframe
+          src="/api/druid/login.html"
+          width="100%"
+          height="100%"
+          style={{border: "none"}}
+        ></iframe>
+      </div>
+    </PageContainer>
+  );
+}

+ 542 - 0
app/(business)/monitor/job-log/index/[jobid]/page.tsx

@@ -0,0 +1,542 @@
+"use client";
+
+import { fetchApi, fetchFile } from "@/app/_modules/func";
+import {
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  EyeOutlined,
+  ReloadOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import {
+  PageContainer,
+  ProDescriptions,
+  ProTable,
+} from "@ant-design/pro-components";
+import { Button, message, Modal, Space, Tag } from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faCheck,
+  faDownload,
+  faToggleOff,
+  faToggleOn,
+  faXmark,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useRef, useState } from "react";
+
+//查询Job详情
+const queryJobAPI = "/api/monitor/job";
+//查询表格数据API
+const queryAPI = "/api/monitor/jobLog/list";
+//删除API
+const deleteAPI = "/api/monitor/jobLog";
+//导出API
+const exportAPI = "/api/monitor/jobLog/export";
+//导出文件前缀名
+const exportFilePrefix = "joblog";
+
+//清空调度日志API
+const clearAllAPI = "/api/monitor/jobLog/clean";
+
+export default function JobLog({ params }: { params: { jobid: string } }) {
+  const { push } = useRouter();
+
+  //获取对应的任务的JobName的值
+  const getJobName = async () => {
+    const resp = await fetchApi(`${queryJobAPI}/${params.jobid}`, push);
+    if (resp != undefined) {
+      if (searchTableFormRef.current) {
+        searchTableFormRef.current.setFieldsValue({
+          jobName: resp.data.jobName,
+          jobGroup: resp.data.jobGroup,
+        });
+      }
+      return [resp.data.jobName, resp.data.jobGroup];
+    }
+
+    return "";
+  };
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "日志编号",
+      dataIndex: "jobLogId",
+      search: false,
+    },
+    {
+      title: "任务名称",
+      fieldProps: {
+        placeholder: "请输入任务名称",
+      },
+      dataIndex: "jobName",
+      ellipsis: true,
+      order: 4,
+    },
+    {
+      title: "任务组名",
+      dataIndex: "jobGroup",
+      valueType: "select",
+      valueEnum: {
+        DEFAULT: {
+          text: "默认",
+          status: "DEFAULT",
+        },
+        SYSTEM: {
+          text: "系统",
+          status: "SYSTEM",
+        },
+      },
+      order: 3,
+    },
+    {
+      title: "调用目标字符串",
+      dataIndex: "invokeTarget",
+      ellipsis: true,
+      search: false,
+    },
+    {
+      title: "日志信息",
+      dataIndex: "jobMessage",
+      ellipsis: true,
+      search: false,
+    },
+    {
+      title: "执行状态",
+      dataIndex: "status",
+      valueType: "select",
+      render: (_, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.status === "0" ? "green" : "red"}
+              icon={
+                record.status == 0 ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {_}
+            </Tag>
+          </Space>
+        );
+      },
+      valueEnum: {
+        0: {
+          text: "成功",
+          status: "0",
+        },
+        1: {
+          text: "失败",
+          status: "1",
+        },
+      },
+      order: 2,
+    },
+    {
+      title: "执行时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      search: false,
+    },
+    {
+      title: "执行时间",
+      fieldProps: {
+        placeholder: ["开始日期", "结束日期"],
+      },
+      dataIndex: "createTimeRange",
+      valueType: "dateRange",
+      hideInTable: true,
+      order: 1,
+      search: {
+        transform: (value) => {
+          return {
+            "params[beginTime]": `${value[0]} 00:00:00`,
+            "params[endTime]": `${value[1]} 23:59:59`,
+          };
+        },
+      },
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => [
+        <Button
+          key="detailBtn"
+          type="link"
+          icon={<EyeOutlined />}
+          onClick={() => onClickShowRowDetailModal(record)}
+        >
+          详情
+        </Button>,
+      ],
+    },
+  ];
+
+  //0.查询表格数据
+  const queryTableData = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    //如果没有带上默认的字典类型,查询绑定上
+    if (!("jobName" in searchParams)) {
+      const result = await getJobName();
+      queryParams.append("jobName", result[0]);
+      queryParams.append("jobGroup", result[1]);
+    }
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(`${queryAPI}?${queryParams}`, push);
+
+    return body;
+  };
+
+  //操作当前数据的附加数据
+  const [operatRowData, setOperateRowData] = useState<{
+    [key: string]: any;
+  }>({});
+
+  //3.删除
+
+  //点击删除按钮,展示删除确认框
+  const onClickDeleteRow = (record?: any) => {
+    const jobLogId =
+      record != undefined ? record.jobLogId : selectedRowKeys.join(",");
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定删除调度日志编号为“${jobLogId}”的数据项?`,
+      onOk() {
+        executeDeleteRow(jobLogId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的数据
+  const executeDeleteRow = async (jobLogId: any) => {
+    const body = await fetchApi(`${deleteAPI}/${jobLogId}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+
+        //删除按钮变回不可点击
+        setRowCanDelete(false);
+        //选中行数据重置为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //弹出清空确认框
+  const onClickClearAll = () => {
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定清空所有调度日志数据项?`,
+      onOk() {
+        executeClearAll();
+      },
+      onCancel() {},
+    });
+  };
+
+  //执行清空调度日志
+  const executeClearAll = async () => {
+    const body = await fetchApi(clearAllAPI, push, {
+      method: "DELETE",
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("清空成功");
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    } else {
+      message.error("清空发生异常");
+    }
+  };
+
+  //4.导出
+
+  //导出表格数据
+  const exportTable = async () => {
+    if (searchTableFormRef.current) {
+      const formData = new FormData();
+
+      const data = {
+        pageNum: page,
+        pageSize: pageSize,
+        ...searchTableFormRef.current.getFieldsValue(),
+      };
+
+      Object.keys(data).forEach((key) => {
+        if (data[key] !== undefined) {
+          formData.append(key, data[key]);
+        }
+      });
+
+      await fetchFile(
+        exportAPI,
+        push,
+        {
+          method: "POST",
+          body: formData,
+        },
+        `${exportFilePrefix}_${new Date().getTime()}.xlsx`
+      );
+    }
+  };
+
+  //5.选择行
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [selectedRow, setSelectedRow] = useState(undefined as any);
+
+  //删除按钮是否可用,选中行时才可用
+  const [rowCanDelete, setRowCanDelete] = useState(false);
+
+  //ProTable rowSelection
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[], selectedRows: any[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+
+      if (newSelectedRowKeys && newSelectedRowKeys.length == 1) {
+        setSelectedRow(selectedRows[0]);
+      } else {
+        setSelectedRow(undefined);
+      }
+    },
+
+    //复选框的额外禁用判断
+    // getCheckboxProps: (record) => ({
+    //   disabled: record.userId == 1,
+    // }),
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionTableRef = useRef<ActionType>();
+  //搜索表单对象引用
+  const searchTableFormRef = useRef<ProFormInstance>();
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  const [isShowDetail, setIsShowDetail] = useState(false);
+
+  //展示详情框
+  const onClickShowRowDetailModal = (record: any) => {
+    setIsShowDetail(true);
+    setSelectedRow(record);
+  };
+
+  return (
+    <PageContainer
+      header={{
+        title: "调度日志",
+        onBack(e) {
+          push("/monitor/job");
+        },
+      }}
+    >
+      <ProTable
+        formRef={searchTableFormRef}
+        rowKey="jobLogId"
+        rowSelection={{
+          selectedRowKeys,
+          ...rowSelection,
+        }}
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await queryTableData(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+          onChange: pageChange,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionTableRef}
+        toolbar={{
+          actions: [
+            <Button
+              key="danger"
+              danger
+              icon={<DeleteOutlined />}
+              disabled={!rowCanDelete}
+              onClick={() => onClickDeleteRow()}
+            >
+              删除
+            </Button>,
+            <Button
+              key="danger"
+              danger
+              icon={<DeleteOutlined />}
+              onClick={() => onClickClearAll()}
+            >
+              清空
+            </Button>,
+            <Button
+              key="export"
+              type="primary"
+              icon={<FontAwesomeIcon icon={faDownload} />}
+              onClick={exportTable}
+            >
+              导出
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionTableRef.current) {
+                  actionTableRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+      {selectedRow !== undefined && (
+        <Modal
+          title="调度日志详情"
+          footer={<Button onClick={() => setIsShowDetail(false)}>关闭</Button>}
+          open={isShowDetail}
+          onCancel={() => setIsShowDetail(false)}
+        >
+          <ProDescriptions column={2}>
+            <ProDescriptions.Item label="日志序号">
+              {selectedRow.jobLogId}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item
+              label="任务分组"
+              valueEnum={{
+                DEFAULT: {
+                  text: "默认",
+                  status: "DEFAULT",
+                },
+                SYSTEM: {
+                  text: "系统",
+                  status: "SYSTEM",
+                },
+              }}
+            >
+              {selectedRow.jobGroup}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item label="任务名称">
+              {selectedRow.jobName}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item label="执行时间">
+              {selectedRow.createTime}
+            </ProDescriptions.Item>
+          </ProDescriptions>
+          <ProDescriptions column={1}>
+            <ProDescriptions.Item label="调用目标方法">
+              {selectedRow.invokeTarget}
+            </ProDescriptions.Item>
+            <ProDescriptions column={1}>
+              <ProDescriptions.Item label="日志信息">
+                {selectedRow.jobMessage}
+              </ProDescriptions.Item>
+              <ProDescriptions.Item
+                label="执行状态"
+                valueEnum={{
+                  0: {
+                    text: "正常",
+                    status: "0",
+                  },
+                  1: {
+                    text: "暂停",
+                    status: "1",
+                  },
+                }}
+              >
+                {selectedRow.status}
+              </ProDescriptions.Item>
+            </ProDescriptions>
+          </ProDescriptions>
+        </Modal>
+      )}
+    </PageContainer>
+  );
+}

+ 1103 - 0
app/(business)/monitor/job/page.tsx

@@ -0,0 +1,1103 @@
+"use client";
+
+import { fetchApi, fetchFile } from "@/app/_modules/func";
+import {
+  CaretDownOutlined,
+  CheckOutlined,
+  CloseOutlined,
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  EyeOutlined,
+  PlusOutlined,
+  ReloadOutlined,
+  SearchOutlined,
+  KeyOutlined,
+  LoadingOutlined,
+  CloudUploadOutlined,
+  FileAddOutlined,
+  ClockCircleOutlined,
+  ScheduleOutlined,
+  PlayCircleOutlined,
+} from "@ant-design/icons";
+import type {
+  ProColumns,
+  ProFormInstance,
+  ActionType,
+} from "@ant-design/pro-components";
+import {
+  ModalForm,
+  PageContainer,
+  ProCard,
+  ProForm,
+  ProFormRadio,
+  ProFormSelect,
+  ProFormText,
+  ProFormTextArea,
+  ProFormTreeSelect,
+  ProTable,
+  ProDescriptions,
+} from "@ant-design/pro-components";
+import type { TreeDataNode, MenuProps, UploadProps, GetProp } from "antd";
+import {
+  Button,
+  Col,
+  Flex,
+  Input,
+  message,
+  Modal,
+  Row,
+  Space,
+  Spin,
+  Switch,
+  Tree,
+  Dropdown,
+  Form,
+  Upload,
+  Typography,
+  Checkbox,
+  Tag,
+} from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faDownload,
+  faPenToSquare,
+  faToggleOff,
+  faToggleOn,
+  faUpload,
+  faUsers,
+  faCheck,
+  faXmark,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import "./styles.scss";
+
+import { ReQuartzCron, ReUnixCron, CronLocalization } from "@sbzen/re-cron";
+
+import { useEffect, useMemo, useRef, useState } from "react";
+import { Divider } from "@/node_modules/antd/es/index";
+import { MortnonCronLocalization } from "@/app/_modules/definies";
+
+//查询表格数据API
+const queryAPI = "/api/monitor/job/list";
+//新建数据API
+const newAPI = "/api/monitor/job";
+//修改数据API
+const modifyAPI = "/api/monitor/job";
+//查询详情数据API
+const queryDetailAPI = "/api/monitor/job";
+//删除API
+const deleteAPI = "/api/monitor/job";
+//导出API
+const exportAPI = "/api/monitor/job/export";
+//导出文件前缀名
+const exportFilePrefix = "job";
+//变更任务状态API
+const changeJobStatusAPI = "/api/monitor/job/changeStatus";
+//执行任务API
+const runAPI = "/api/monitor/job/run";
+
+export default function Job() {
+  const { push } = useRouter();
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "任务编号",
+      dataIndex: "jobId",
+      search: false,
+    },
+    {
+      title: "任务名称",
+      fieldProps: {
+        placeholder: "请输入任务名称",
+      },
+      dataIndex: "jobName",
+      ellipsis: true,
+      order: 3,
+    },
+    {
+      title: "任务组名",
+      dataIndex: "jobGroup",
+      valueType: "select",
+      valueEnum: {
+        DEFAULT: {
+          text: "默认",
+          status: "DEFAULT",
+        },
+        SYSTEM: {
+          text: "系统",
+          status: "SYSTEM",
+        },
+      },
+      sorter: true,
+      order: 2,
+    },
+    {
+      title: "调用目标字符串",
+      dataIndex: "invokeTarget",
+      search: false,
+    },
+    {
+      title: "Cron执行表达式",
+      dataIndex: "cronExpression",
+      search: false,
+    },
+    {
+      title: "状态",
+      fieldProps: {
+        placeholder: "请选择任务状态",
+      },
+      dataIndex: "status",
+      valueType: "select",
+      order: 2,
+      valueEnum: {
+        0: {
+          text: "正常",
+          status: "0",
+        },
+        1: {
+          text: "停用",
+          status: "1",
+        },
+      },
+      render: (text, record) => {
+        return (
+          <Space>
+            <Switch
+              checkedChildren={<CheckOutlined />}
+              unCheckedChildren={<CloseOutlined />}
+              defaultChecked={record.status === "0"}
+              checked={rowStatusMap[record.jobId]}
+              onChange={(checked, event) => {
+                showSwitchJobStatusModal(checked, record);
+              }}
+            />
+          </Space>
+        );
+      },
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => [
+        <Button
+          key="modifyBtn"
+          type="link"
+          icon={<FontAwesomeIcon icon={faPenToSquare} />}
+          onClick={() => onClickShowRowModifyModal(record)}
+        >
+          修改
+        </Button>,
+        <Button
+          key="deleteBtn"
+          type="link"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => onClickDeleteRow(record)}
+        >
+          删除
+        </Button>,
+        <Dropdown
+          key="moreDrop"
+          menu={{
+            items: [
+              {
+                key: "1",
+                label: (
+                  <a
+                    onClick={() => {
+                      showRunOnceModal(record);
+                    }}
+                  >
+                    执行一次
+                  </a>
+                ),
+                icon: <PlayCircleOutlined />,
+              },
+              {
+                key: "2",
+                label: <a onClick={() => showRowModal(record)}>任务详情</a>,
+                icon: <EyeOutlined />,
+              },
+              {
+                key: "3",
+                label: (
+                  <a
+                    onClick={() =>
+                      push(`/monitor/job-log/index/${record.jobId}`)
+                    }
+                  >
+                    调度日志
+                  </a>
+                ),
+                icon: <ScheduleOutlined />,
+              },
+            ],
+          }}
+        >
+          <a onClick={(e) => e.preventDefault()}>
+            <Space>
+              更多
+              <CaretDownOutlined />
+            </Space>
+          </a>
+        </Dropdown>,
+      ],
+    },
+  ];
+
+  //0.查询表格数据
+  const queryTableData = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(`${queryAPI}?${queryParams}`, push);
+
+    if (body !== undefined) {
+      body.rows.forEach((row: any) => {
+        setRowStatusMap({ ...rowStatusMap, [row.userId]: row.status === "0" });
+      });
+    }
+
+    return body;
+  };
+
+  //1.新建
+
+  //新建对话框表单引用
+  const addFormRef = useRef<ProFormInstance>();
+
+  //确定新建数据
+  const executeAddData = async (values: any) => {
+    const body = await fetchApi(newAPI, push, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        return true;
+      }
+
+      message.error(body.msg);
+      return false;
+    }
+    return false;
+  };
+
+  //2.修改
+
+  //控制行的状态值的恢复
+  const [rowStatusMap, setRowStatusMap] = useState<{ [key: number]: boolean }>(
+    {}
+  );
+
+  //展示切换任务状态对话框
+  const showSwitchJobStatusModal = (checked: boolean, record: any) => {
+    setRowStatusMap({ ...rowStatusMap, [record.jobId]: checked });
+
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确认要${checked ? "启用" : "停用"}"${record.jobName}"任务吗?`,
+      onOk() {
+        executeSwitchStatus(checked, record.jobId, () => {
+          setRowStatusMap({ ...rowStatusMap, [record.jobId]: !checked });
+        });
+      },
+      onCancel() {
+        setRowStatusMap({ ...rowStatusMap, [record.jobId]: !checked });
+      },
+    });
+  };
+
+  //确认变更任务状态
+  const executeSwitchStatus = async (
+    checked: boolean,
+    jobId: string,
+    erroCallback: () => void
+  ) => {
+    const modifyData = {
+      jobId: jobId,
+      status: checked ? "0" : "1",
+    };
+    const body = await fetchApi(changeJobStatusAPI, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(modifyData),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+      } else {
+        message.error(body.msg);
+        erroCallback();
+      }
+    }
+  };
+
+  //是否展示修改对话框
+  const [isShowModifyDataModal, setIsShowModifyDataModal] = useState(false);
+
+  //展示修改对话框
+  const onClickShowRowModifyModal = (record?: any) => {
+    queryRowData(record);
+    setIsShowModifyDataModal(true);
+  };
+
+  //修改数据表单引用
+  const modifyFormRef = useRef<ProFormInstance>();
+
+  //操作当前数据的附加数据
+  const [operatRowData, setOperateRowData] = useState<{
+    [key: string]: any;
+  }>({});
+
+  //查询并加载待修改数据的详细信息
+  const queryRowData = async (record?: any) => {
+    const jobId = record !== undefined ? record.jobId : selectedRow.jobId;
+
+    operatRowData["jobId"] = jobId;
+
+    setOperateRowData(operatRowData);
+
+    if (jobId !== undefined) {
+      const body = await fetchApi(`${queryDetailAPI}/${jobId}`, push);
+
+      if (body !== undefined) {
+        if (body.code == 200) {
+          modifyFormRef?.current?.setFieldsValue({
+            //需要加载到修改表单中的数据
+            jobName: body.data.jobName,
+            jobGroup: body.data.jobGroup,
+            invokeTarget: body.data.invokeTarget,
+            cronExpression: body.data.cronExpression,
+            status: body.data.status,
+            misfirePolicy: body.data.misfirePolicy,
+            concurrent: body.data.concurrent,
+          });
+
+          setCronValue(body.data.cronExpression);
+        }
+      }
+    }
+  };
+
+  //确认修改数据
+  const executeModifyData = async (values: any) => {
+    values["jobId"] = operatRowData["jobId"];
+
+    const body = await fetchApi(modifyAPI, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        setIsShowModifyDataModal(false);
+        return true;
+      }
+      message.error(body.msg);
+      return false;
+    }
+  };
+
+  //3.删除
+
+  //点击删除按钮,展示删除确认框
+  const onClickDeleteRow = (record?: any) => {
+    const jobId =
+      record != undefined ? record.jobId : selectedRowKeys.join(",");
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定删除任务编号为“${jobId}”的数据项?`,
+      onOk() {
+        executeDeleteRow(jobId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的数据
+  const executeDeleteRow = async (jobId: any) => {
+    const body = await fetchApi(`${deleteAPI}/${jobId}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+
+        //修改按钮变回不可点击
+        setRowCanModify(false);
+        //删除按钮变回不可点击
+        setRowCanDelete(false);
+        //选中行数据重置为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //4.导出
+
+  //导出表格数据
+  const exportTable = async () => {
+    if (searchTableFormRef.current) {
+      const formData = new FormData();
+
+      const data = {
+        pageNum: page,
+        pageSize: pageSize,
+        ...searchTableFormRef.current.getFieldsValue(),
+      };
+
+      Object.keys(data).forEach((key) => {
+        if (data[key] !== undefined) {
+          formData.append(key, data[key]);
+        }
+      });
+
+      await fetchFile(
+        exportAPI,
+        push,
+        {
+          method: "POST",
+          body: formData,
+        },
+        `${exportFilePrefix}_${new Date().getTime()}.xlsx`
+      );
+    }
+  };
+
+  //5.选择行
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [selectedRow, setSelectedRow] = useState(undefined as any);
+
+  //修改按钮是否可用,选中行时才可用
+  const [rowCanModify, setRowCanModify] = useState(false);
+
+  //删除按钮是否可用,选中行时才可用
+  const [rowCanDelete, setRowCanDelete] = useState(false);
+
+  //ProTable rowSelection
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[], selectedRows: any[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+
+      if (newSelectedRowKeys && newSelectedRowKeys.length == 1) {
+        setSelectedRow(selectedRows[0]);
+        setRowCanModify(true);
+      } else {
+        setRowCanModify(false);
+        setSelectedRow(undefined);
+      }
+    },
+
+    //复选框的额外禁用判断
+    // getCheckboxProps: (record) => ({
+    //   disabled: record.userId == 1,
+    // }),
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionTableRef = useRef<ActionType>();
+  //搜索表单对象引用
+  const searchTableFormRef = useRef<ProFormInstance>();
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  //是否展示Cron表达式生成框
+  const [isCronShow, setIsCronShow] = useState(false);
+
+  //Cron表达式值
+  const [cronValue, setCronValue] = useState("");
+
+  //当前是新建还是修改触发的Cron生成框
+  const [isNew, setIsNew] = useState(true);
+
+  //用于重置Cron生成框的key值
+  const [modalKey, setModalKey] = useState(0);
+
+  //展示Cron对话框,区分新建用还是修改用
+  const showCronModal = (isNew: boolean) => {
+    setIsNew(isNew);
+    setIsCronShow(true);
+  };
+
+  //回写Cron数据
+  const getCronData = () => {
+    setIsCronShow(false);
+    if (isNew) {
+      addFormRef?.current?.setFieldsValue({
+        cronExpression: cronValue,
+      });
+    } else {
+      modifyFormRef?.current?.setFieldsValue({
+        cronExpression: cronValue,
+      });
+    }
+    //重置Cron数据
+    setCronValue("");
+    setModalKey((preKey) => preKey + 1);
+  };
+
+  //执行任务一次
+  const showRunOnceModal = (record: any) => {
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定要立即执行一次任务“${record.jobName}”吗?`,
+      onOk() {
+        executeJob(record);
+      },
+      onCancel() {},
+    });
+  };
+
+  //执行任务
+  const executeJob = async (record: any) => {
+    const runData = {
+      jobId: record.jobId,
+      jobGroup: record.jobGroup,
+    };
+
+    const body = await fetchApi(runAPI, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(runData),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("执行成功");
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //是否展示任务详情框
+  const [isShowDetail, setIsShowDetail] = useState(false);
+
+  //展示行详情框
+  const showRowModal = async (record: any) => {
+    const jobId = record.jobId;
+    if (jobId !== undefined) {
+      const body = await fetchApi(`${queryDetailAPI}/${jobId}`, push);
+      setSelectedRow(body.data);
+    }
+
+    setIsShowDetail(true);
+  };
+
+  return (
+    <PageContainer title={false}>
+      <ProTable
+        formRef={searchTableFormRef}
+        rowKey="jobId"
+        rowSelection={{
+          selectedRowKeys,
+          ...rowSelection,
+        }}
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await queryTableData(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+          onChange: pageChange,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionTableRef}
+        toolbar={{
+          actions: [
+            <ModalForm
+              formRef={addFormRef}
+              key="addmodal"
+              layout="horizontal"
+              title="添加任务"
+              trigger={
+                <Button icon={<PlusOutlined />} type="primary">
+                  新建
+                </Button>
+              }
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+              }}
+              submitTimeout={2000}
+              onFinish={executeAddData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="jobName"
+                  label="任务名称"
+                  placeholder="请输入任务名称"
+                  rules={[{ required: true, message: "请输入任务名称" }]}
+                />
+                <ProFormSelect
+                  width="md"
+                  name="jobGroup"
+                  label="任务分组"
+                  valueEnum={{
+                    DEFAULT: {
+                      text: "默认",
+                      status: "DEFAULT",
+                    },
+                    SYSTEM: {
+                      text: "系统",
+                      status: "SYSTEM",
+                    },
+                  }}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormText
+                  width="lg"
+                  name="invokeTarget"
+                  label="调用方法"
+                  placeholder="请输入调用方法的字符串"
+                  tooltip="Bean调用示例:ryTask.ryParams('ry')
+                  Class调用示例:com.ruoyi.quartz.task.RyTask.ryParams('ry')
+                  参数说明:支持字符串,布尔类型,长整型,浮点型,整型"
+                  rules={[
+                    { required: true, message: "请输入调用方法的字符串" },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <Space.Compact>
+                  <ProFormText
+                    width="lg"
+                    name="cronExpression"
+                    label="Cron表达式"
+                    placeholder="请输入Cron表达式"
+                    rules={[{ required: true, message: "请输入Cron表达式" }]}
+                  />
+                  <Button
+                    icon={<ClockCircleOutlined />}
+                    onClick={() => showCronModal(true)}
+                  >
+                    生成表达式
+                  </Button>
+                </Space.Compact>
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormRadio.Group
+                  width="md"
+                  name="misfirePolicy"
+                  label="执行策略"
+                  initialValue="1"
+                  options={[
+                    {
+                      label: "立即执行",
+                      value: "1",
+                    },
+                    {
+                      label: "执行一次",
+                      value: "2",
+                    },
+                    {
+                      label: "放弃执行",
+                      value: "3",
+                    },
+                  ]}
+                />
+                <ProFormRadio.Group
+                  name="concurrent"
+                  width="md"
+                  label="是否并发"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "允许",
+                      value: "0",
+                    },
+                    {
+                      label: "禁止",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+            </ModalForm>,
+            <ModalForm
+              key="modifymodal"
+              title="修改岗位"
+              layout="horizontal"
+              formRef={modifyFormRef}
+              trigger={
+                <Button
+                  icon={<FontAwesomeIcon icon={faPenToSquare} />}
+                  disabled={!rowCanModify}
+                  onClick={() => onClickShowRowModifyModal()}
+                >
+                  修改
+                </Button>
+              }
+              open={isShowModifyDataModal}
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+                onCancel: () => {
+                  setIsShowModifyDataModal(false);
+                },
+              }}
+              submitTimeout={2000}
+              onFinish={executeModifyData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="jobName"
+                  label="任务名称"
+                  placeholder="请输入任务名称"
+                  rules={[{ required: true, message: "请输入任务名称" }]}
+                />
+                <ProFormSelect
+                  width="md"
+                  name="jobGroup"
+                  label="任务分组"
+                  valueEnum={{
+                    DEFAULT: {
+                      text: "默认",
+                      status: "DEFAULT",
+                    },
+                    SYSTEM: {
+                      text: "系统",
+                      status: "SYSTEM",
+                    },
+                  }}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormText
+                  width="lg"
+                  name="invokeTarget"
+                  label="调用方法"
+                  placeholder="请输入调用方法的字符串"
+                  tooltip="Bean调用示例:ryTask.ryParams('ry')
+                  Class调用示例:com.ruoyi.quartz.task.RyTask.ryParams('ry')
+                  参数说明:支持字符串,布尔类型,长整型,浮点型,整型"
+                  rules={[
+                    { required: true, message: "请输入调用方法的字符串" },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <Space.Compact>
+                  <ProFormText
+                    width="lg"
+                    name="cronExpression"
+                    label="Cron表达式"
+                    placeholder="请输入Cron表达式"
+                    rules={[{ required: true, message: "请输入Cron表达式" }]}
+                  />
+                  <Button
+                    icon={<ClockCircleOutlined />}
+                    onClick={() => showCronModal(false)}
+                  >
+                    生成表达式
+                  </Button>
+                </Space.Compact>
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormRadio.Group
+                  name="status"
+                  width="md"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "暂停",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormRadio.Group
+                  width="md"
+                  name="misfirePolicy"
+                  label="执行策略"
+                  initialValue="1"
+                  options={[
+                    {
+                      label: "立即执行",
+                      value: "1",
+                    },
+                    {
+                      label: "执行一次",
+                      value: "2",
+                    },
+                    {
+                      label: "放弃执行",
+                      value: "3",
+                    },
+                  ]}
+                />
+                <ProFormRadio.Group
+                  name="concurrent"
+                  width="md"
+                  label="是否并发"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "允许",
+                      value: "0",
+                    },
+                    {
+                      label: "禁止",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+            </ModalForm>,
+
+            <Button
+              key="danger"
+              danger
+              icon={<DeleteOutlined />}
+              disabled={!rowCanDelete}
+              onClick={() => onClickDeleteRow()}
+            >
+              删除
+            </Button>,
+            <Button
+              key="export"
+              type="primary"
+              icon={<FontAwesomeIcon icon={faDownload} />}
+              onClick={exportTable}
+            >
+              导出
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionTableRef.current) {
+                  actionTableRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+      <Modal
+        title="Cron表达式生成器"
+        key={modalKey}
+        zIndex={10000}
+        open={isCronShow}
+        onOk={getCronData}
+        onCancel={() => setIsCronShow(false)}
+        // confirmLoading={confirmLoading}
+      >
+        <Input prefix={<ClockCircleOutlined />} value={cronValue} />
+        <div style={{ maxHeight: 450, overflow: "auto" }}>
+          <ReQuartzCron
+            cssClassPrefix="cron-"
+            localization={MortnonCronLocalization}
+            value={cronValue}
+            onChange={setCronValue}
+            renderYearsFrom={new Date().getFullYear()}
+          />
+        </div>
+      </Modal>
+      {selectedRow !== undefined && (
+        <Modal
+          title="任务详情"
+          footer={<Button onClick={() => setIsShowDetail(false)}>关闭</Button>}
+          open={isShowDetail}
+          onCancel={() => setIsShowDetail(false)}
+        >
+          <ProDescriptions column={2}>
+            <ProDescriptions.Item label="任务编号">
+              {selectedRow.jobId}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item
+              label="任务分组"
+              valueEnum={{
+                DEFAULT: {
+                  text: "默认",
+                  status: "DEFAULT",
+                },
+                SYSTEM: {
+                  text: "系统",
+                  status: "SYSTEM",
+                },
+              }}
+            >
+              {selectedRow.jobGroup}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item label="任务名称">
+              {selectedRow.jobName}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item label="创建时间">
+              {selectedRow.createTime}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item label="Cron表达式">
+              {selectedRow.cronExpression}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item label="下次执行时间">
+              {selectedRow.nextValidTime}
+            </ProDescriptions.Item>
+          </ProDescriptions>
+          <ProDescriptions column={1}>
+            <ProDescriptions.Item label="调用目标方法">
+              {selectedRow.invokeTarget}
+            </ProDescriptions.Item>
+          </ProDescriptions>
+          <ProDescriptions column={2}>
+            <ProDescriptions.Item
+              label="任务状态"
+              valueEnum={{
+                0: {
+                  text: "正常",
+                  status: "0",
+                },
+                1: {
+                  text: "暂停",
+                  status: "1",
+                },
+              }}
+            >
+              {selectedRow.status}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item
+              label="是否并发"
+              valueEnum={{
+                0: {
+                  text: "允许",
+                  status: "0",
+                },
+                1: {
+                  text: "禁止",
+                  status: "1",
+                },
+              }}
+            >
+              {selectedRow.concurrent}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item
+              label="执行策略"
+              valueEnum={{
+                1: {
+                  text: "立即执行",
+                  status: "1",
+                },
+                2: {
+                  text: "执行一次",
+                  status: "2",
+                },
+                3: {
+                  text: "放弃执行",
+                  status: "3",
+                },
+              }}
+            >
+              {selectedRow.misfirePolicy}
+            </ProDescriptions.Item>
+          </ProDescriptions>
+        </Modal>
+      )}
+    </PageContainer>
+  );
+}

+ 49 - 0
app/(business)/monitor/job/styles.scss

@@ -0,0 +1,49 @@
+$prefix: '.cron';
+
+#{$prefix}-row {
+  overflow: hidden;
+
+  #{$prefix}-col-1 {
+    width: calc(100% / 12);
+    float: left;
+  }
+
+  #{$prefix}-col-2 {
+    width: calc(100% / 6);
+    float: left;
+  }
+}
+
+#{$prefix}-form-inline {
+  display: flex;
+  flex-direction: row;
+}
+
+#{$prefix}-form-control {
+  margin: 0 .2rem;
+}
+
+#{$prefix}-form-check-label {
+  padding-left: .4rem;
+}
+
+#{$prefix}-nav-tabs {
+  list-style: none;
+  display: flex;
+  padding:0;
+  border-bottom: 1px solid lightgray;
+}
+
+#{$prefix}-nav-item {
+  // margin-right: 10px;
+}
+
+#{$prefix}-nav-link {
+  padding: 10px;
+  background-color: rgba($color: #ffffff, $alpha: 1.0);
+  border: none;
+}
+
+#{$prefix}-active {
+  border-bottom: 2px solid #1677ff;
+}

+ 244 - 0
app/(business)/monitor/online/page.tsx

@@ -0,0 +1,244 @@
+"use client";
+
+import { fetchApi } from "@/app/_modules/func";
+import {
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  ReloadOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import { PageContainer, ProTable } from "@ant-design/pro-components";
+import { Button, Modal, message } from "antd";
+import { useRouter } from "next/navigation";
+
+import { faToggleOff, faToggleOn } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useRef, useState } from "react";
+
+//查询表格数据API
+const queryAPI = "/api/monitor/online/list";
+//强退用户API
+const logoutAPI = "/api/monitor/online";
+
+export default function Online() {
+  const { push } = useRouter();
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "序号",
+      dataIndex: "index",
+      valueType: "index",
+    },
+    {
+      title: "会话编号",
+      dataIndex: "tokenId",
+      ellipsis: true,
+      search: false,
+    },
+    {
+      title: "用户名称",
+      fieldProps: {
+        placeholder: "请输入用户名称",
+      },
+      dataIndex: "userName",
+      order: 1,
+    },
+    {
+      title: "部门名称",
+      dataIndex: "deptName",
+      search: false,
+    },
+    {
+      title: "IP地址",
+      fieldProps: {
+        placeholder: "请输入IP地址",
+      },
+      dataIndex: "ipaddr",
+      order: 2,
+    },
+    {
+      title: "登录地点",
+      dataIndex: "loginLocation",
+      search: false,
+    },
+    {
+      title: "浏览器",
+      dataIndex: "browser",
+      search: false,
+    },
+    {
+      title: "操作系统",
+      dataIndex: "os",
+      search: false,
+    },
+    {
+      title: "登录时间",
+      dataIndex: "loginTime",
+      valueType: "dateTime",
+      search: false,
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => [
+        <Button
+          key="deleteBtn"
+          type="link"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => onClickLogoutRow(record)}
+        >
+          强退
+        </Button>,
+      ],
+    },
+  ];
+
+  //0.查询表格数据
+  const queryTableData = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    return await fetchApi(`${queryAPI}?${queryParams}`, push);
+  };
+
+  //3.删除
+
+  //点击删除按钮,展示删除确认框
+  const onClickLogoutRow = (record?: any) => {
+    const tokenId = record.tokenId;
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定强退登录名称为“${record.userName}”的用户?`,
+      onOk() {
+        executeLogoutRow(tokenId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定强退选中的会话
+  const executeLogoutRow = async (tokenId: any) => {
+    const body = await fetchApi(`${logoutAPI}/${tokenId}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("强退成功");
+
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionTableRef = useRef<ActionType>();
+  //搜索表单对象引用
+  const searchTableFormRef = useRef<ProFormInstance>();
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  return (
+    <PageContainer title={false}>
+      <ProTable
+        formRef={searchTableFormRef}
+        rowKey="tokenId"
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await queryTableData(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+          onChange: pageChange,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionTableRef}
+        toolbar={{
+          actions: [],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionTableRef.current) {
+                  actionTableRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+    </PageContainer>
+  );
+}

+ 289 - 0
app/(business)/monitor/server/page.tsx

@@ -0,0 +1,289 @@
+"use client";
+
+import { fetchApi } from "@/app/_modules/func";
+import { PageContainer, ProCard } from "@ant-design/pro-components";
+import { Statistic, Divider, Table } from "antd";
+import type { TableProps } from "antd";
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+
+import {
+  faMicrochip,
+  faMemory,
+  faServer,
+  faMugHot,
+  faHardDrive,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Col, Row } from "@/node_modules/antd/es/index";
+
+import CountUp from "react-countup";
+
+//查询API
+const queryAPI = "/api/monitor/server";
+
+export default function Server() {
+  const { push } = useRouter();
+  const [data, setData] = useState(undefined as any);
+
+  const columns: TableProps["columns"] = [
+    {
+      title: "盘符路径",
+      dataIndex: "dirName",
+      key: "dirName",
+    },
+    {
+      title: "文件系统",
+      dataIndex: "sysTypeName",
+      key: "sysTypeName",
+    },
+    {
+      title: "盘符类型",
+      dataIndex: "typeName",
+      key: "typeName",
+    },
+    {
+      title: "总大小",
+      dataIndex: "total",
+      key: "total",
+    },
+    {
+      title: "可用大小",
+      dataIndex: "free",
+      key: "free",
+    },
+    {
+      title: "已用大小",
+      dataIndex: "used",
+      key: "used",
+    },
+    {
+      title: "已用百分比",
+      dataIndex: "usage",
+      key: "usage",
+      render: (text) => <>{text}%</>,
+    },
+  ];
+
+  //查询数据
+  const queryData = async () => {
+    const body = await fetchApi(queryAPI, push);
+    if (body !== undefined) {
+      if (body.code == 200) {
+        console.log("data:", body.data);
+        setData(body.data);
+      }
+    }
+  };
+
+  useEffect(() => {
+    queryData();
+  }, []);
+
+  const formatterDecimal = (value: any) => (
+    <CountUp end={parseFloat(value)} separator="," decimals={2} />
+  );
+
+  return (
+    <PageContainer title={false}>
+      {data !== undefined && (
+        <ProCard gutter={[16, 16]} style={{ marginBlockStart: 16 }} wrap>
+          <ProCard
+            title={
+              <>
+                <FontAwesomeIcon icon={faMicrochip} />
+                <span style={{ marginLeft: 6 }}>CPU</span>
+              </>
+            }
+            direction="row"
+            headerBordered
+            bordered
+            hoverable
+          >
+            <ProCard>
+              <Statistic title="核心数" value={data.cpu.cpuNum} />
+            </ProCard>
+            <Divider type="vertical" />
+            <ProCard>
+              <Statistic
+                title="用户使用率"
+                value={data.cpu.used}
+                suffix="%"
+                formatter={formatterDecimal}
+              />
+            </ProCard>
+            <Divider type="vertical" />
+            <ProCard>
+              <Statistic
+                title="系统使用率"
+                value={data.cpu.sys}
+                suffix="%"
+                formatter={formatterDecimal}
+              />
+            </ProCard>
+            <Divider type="vertical" />
+            <ProCard>
+              <Statistic
+                title="当前空闲率"
+                value={data.cpu.free}
+                suffix="%"
+                formatter={formatterDecimal}
+              />
+            </ProCard>
+          </ProCard>
+
+          <ProCard
+            title={
+              <>
+                <FontAwesomeIcon icon={faMemory} />
+                <span style={{ marginLeft: 6 }}>内存</span>
+              </>
+            }
+            direction="row"
+            headerBordered
+            bordered
+            hoverable
+          >
+            <ProCard title="总内存">
+              <Statistic title="内存" value={data.mem.total} suffix="G" />
+              <Statistic title="JVM" value={data.jvm.total} suffix="M" />
+            </ProCard>
+            <ProCard title="已用内存">
+              <Statistic
+                title="内存"
+                value={data.mem.used}
+                suffix="G"
+                formatter={formatterDecimal}
+              />
+              <Statistic
+                title="JVM"
+                value={data.jvm.used}
+                suffix="M"
+                formatter={formatterDecimal}
+              />
+            </ProCard>
+            <ProCard title="剩余内存">
+              <Statistic
+                title="内存"
+                value={data.mem.free}
+                suffix="G"
+                formatter={formatterDecimal}
+              />
+              <Statistic
+                title="JVM"
+                value={data.jvm.free}
+                suffix="M"
+                formatter={formatterDecimal}
+              />
+            </ProCard>
+            <ProCard title="使用率">
+              <Statistic
+                title="内存"
+                value={data.mem.usage}
+                suffix="%"
+                valueStyle={{
+                  color: data.mem.usage > 80 ? "#cf1322" : "inherit",
+                }}
+                formatter={formatterDecimal}
+              />
+              <Statistic
+                title="JVM"
+                value={data.jvm.usage}
+                suffix="%"
+                valueStyle={{
+                  color: data.jvm.usage > 80 ? "#cf1322" : "inherit",
+                }}
+                formatter={formatterDecimal}
+              />
+            </ProCard>
+          </ProCard>
+
+          <ProCard
+            title={
+              <>
+                <FontAwesomeIcon icon={faServer} />
+                <span style={{ marginLeft: 6 }}>服务器信息</span>
+              </>
+            }
+            direction="row"
+            headerBordered
+            bordered
+            hoverable
+          >
+            <ProCard>
+              <Statistic title="服务器名称" value={data.sys.computerName} />
+            </ProCard>
+            <ProCard>
+              <Statistic title="服务器IP" value={data.sys.computerIp} />
+            </ProCard>
+            <ProCard>
+              <Statistic title="操作系统" value={data.sys.osName} />
+            </ProCard>
+            <ProCard>
+              <Statistic title="系统架构" value={data.sys.osArch} />
+            </ProCard>
+          </ProCard>
+
+          <ProCard
+            title={
+              <>
+                <FontAwesomeIcon icon={faMugHot} />
+                <span style={{ marginLeft: 6 }}>Java虚拟机信息</span>
+              </>
+            }
+            direction="column"
+            headerBordered
+            bordered
+            hoverable
+          >
+            <Row gutter={[0, 16]}>
+              <Col span={12}>
+                <Statistic title="名称" value={data.jvm.name} />
+              </Col>
+              <Col span={12}>
+                <Statistic title="版本" value={data.jvm.version} />
+              </Col>
+            </Row>
+            <Row gutter={[0, 16]}>
+              <Col span={12}>
+                <Statistic title="启动时间" value={data.jvm.startTime} />
+              </Col>
+              <Col span={12}>
+                <Statistic title="运行时长" value={data.jvm.runTime} />
+              </Col>
+            </Row>
+            <Row gutter={[0, 16]}>
+              <Col span={24}>
+                <Statistic title="安装路径" value={data.jvm.home} />
+              </Col>
+            </Row>
+            <Row gutter={[0, 16]}>
+              <Col span={24}>
+                <Statistic title="项目路径" value={data.sys.userDir} />
+              </Col>
+            </Row>
+            <Row gutter={[0, 16]}>
+              <Col span={24}>
+                <Statistic title="运行参数" value={data.jvm.inputArgs} />
+              </Col>
+            </Row>
+          </ProCard>
+
+          <ProCard
+            title={
+              <>
+                <FontAwesomeIcon icon={faHardDrive} />
+                <span style={{ marginLeft: 6 }}>磁盘状态</span>
+              </>
+            }
+            headerBordered
+            bordered
+            hoverable
+          >
+            <Table columns={columns} dataSource={data.sysFiles} />
+          </ProCard>
+        </ProCard>
+      )}
+    </PageContainer>
+  );
+}

+ 9 - 0
app/(business)/styles.css

@@ -0,0 +1,9 @@
+.search-input {
+    opacity: 0;
+    transition: opacity 0.3s;
+}
+
+.search-input.visible {
+    opacity: 1;
+}
+

+ 686 - 0
app/(business)/system/config/page.tsx

@@ -0,0 +1,686 @@
+"use client";
+
+import { fetchApi, fetchFile } from "@/app/_modules/func";
+import {
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  PlusOutlined,
+  ReloadOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import {
+  ModalForm,
+  PageContainer,
+  ProForm,
+  ProFormRadio,
+  ProFormText,
+  ProFormTextArea,
+  ProTable,
+} from "@ant-design/pro-components";
+import { Button, message, Modal, Space, Tag } from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faCheck,
+  faDownload,
+  faPenToSquare,
+  faRotate,
+  faToggleOff,
+  faToggleOn,
+  faXmark,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useRef, useState } from "react";
+
+//查询表格数据API
+const queryAPI = "/api/system/config/list";
+//新建数据API
+const newAPI = "/api/system/config";
+//修改数据API
+const modifyAPI = "/api/system/config";
+//查询详情数据API
+const queryDetailAPI = "/api/system/config";
+//删除API
+const deleteAPI = "/api/system/config";
+//导出API
+const exportAPI = "/api/system/config/export";
+//导出文件前缀名
+const exportFilePrefix = "config";
+//刷新缓存
+const refreshAPI = "/api/system/config/refreshCache";
+
+export default function Config() {
+  const { push } = useRouter();
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "参数编号",
+      dataIndex: "configId",
+      search: false,
+    },
+    {
+      title: "参数名称",
+      fieldProps: {
+        placeholder: "请输入参数名称",
+      },
+      dataIndex: "configName",
+      ellipsis: true,
+      sorter: true,
+      order: 4,
+    },
+    {
+      title: "参数键名",
+      fieldProps: {
+        placeholder: "请输入参数键名",
+      },
+      dataIndex: "configKey",
+      ellipsis: true,
+      order: 3,
+    },
+    {
+      title: "参数键值",
+      dataIndex: "configValue",
+      ellipsis: true,
+      search: false,
+    },
+    {
+      title: "系统内置",
+      fieldProps: {
+        placeholder: "请选择是否系统内置",
+      },
+      dataIndex: "configType",
+      valueType: "select",
+      render: (_, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.configType === "Y" ? "green" : "red"}
+              icon={
+                record.configType === "Y" ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {_}
+            </Tag>
+          </Space>
+        );
+      },
+      valueEnum: {
+        Y: {
+          text: "是",
+          status: "Y",
+        },
+        N: {
+          text: "否",
+          status: "N",
+        },
+      },
+      order: 2,
+    },
+    {
+      title: "备注",
+      dataIndex: "remark",
+      ellipsis: true,
+      search: false,
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      search: false,
+    },
+    {
+      title: "创建时间",
+      fieldProps: {
+        placeholder: ["开始日期", "结束日期"],
+      },
+      dataIndex: "createTimeRange",
+      valueType: "dateRange",
+      hideInTable: true,
+      order: 1,
+      search: {
+        transform: (value) => {
+          return {
+            "params[beginTime]": `${value[0]} 00:00:00`,
+            "params[endTime]": `${value[1]} 23:59:59`,
+          };
+        },
+      },
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => [
+        <Button
+          key="modifyBtn"
+          type="link"
+          icon={<FontAwesomeIcon icon={faPenToSquare} />}
+          onClick={() => onClickShowRowModifyModal(record)}
+        >
+          修改
+        </Button>,
+        <Button
+          key="deleteBtn"
+          type="link"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => onClickDeleteRow(record)}
+        >
+          删除
+        </Button>,
+      ],
+    },
+  ];
+
+  //0.查询表格数据
+  const queryTableData = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(`${queryAPI}?${queryParams}`, push);
+
+    return body;
+  };
+
+  //1.新建
+
+  //确定新建数据
+  const executeAddData = async (values: any) => {
+    const body = await fetchApi(newAPI, push, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        return true;
+      }
+
+      message.error(body.msg);
+      return false;
+    }
+    return false;
+  };
+
+  //2.修改
+
+  //是否展示修改对话框
+  const [isShowModifyDataModal, setIsShowModifyDataModal] = useState(false);
+
+  //展示修改对话框
+  const onClickShowRowModifyModal = (record?: any) => {
+    queryRowData(record);
+    setIsShowModifyDataModal(true);
+  };
+
+  //修改数据表单引用
+  const modifyFormRef = useRef<ProFormInstance>();
+
+  //操作当前数据的附加数据
+  const [operatRowData, setOperateRowData] = useState<{
+    [key: string]: any;
+  }>({});
+
+  //查询并加载待修改数据的详细信息
+  const queryRowData = async (record?: any) => {
+    const configId =
+      record !== undefined ? record.configId : selectedRow.configId;
+
+    operatRowData["configId"] = configId;
+
+    setOperateRowData(operatRowData);
+
+    if (configId !== undefined) {
+      const body = await fetchApi(`${queryDetailAPI}/${configId}`, push);
+
+      if (body !== undefined) {
+        if (body.code == 200) {
+          modifyFormRef?.current?.setFieldsValue({
+            //需要加载到修改表单中的数据
+            configName: body.data.configName,
+            configKey: body.data.configKey,
+            configValue: body.data.configValue,
+            configType: body.data.configType,
+            remark: body.data.remark,
+          });
+        }
+      }
+    }
+  };
+
+  //确认修改数据
+  const executeModifyData = async (values: any) => {
+    values["configId"] = operatRowData["configId"];
+
+    const body = await fetchApi(modifyAPI, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        setIsShowModifyDataModal(false);
+        return true;
+      }
+      message.error(body.msg);
+      return false;
+    }
+  };
+
+  //3.删除
+
+  //点击删除按钮,展示删除确认框
+  const onClickDeleteRow = (record?: any) => {
+    const configId =
+      record != undefined ? record.configId : selectedRowKeys.join(",");
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `是否确认删除参数编号为“${configId}”的数据项?`,
+      onOk() {
+        executeDeleteRow(configId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的数据
+  const executeDeleteRow = async (dictId: any) => {
+    const body = await fetchApi(`${deleteAPI}/${dictId}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+
+        //修改按钮变回不可点击
+        setRowCanModify(false);
+        //删除按钮变回不可点击
+        setRowCanDelete(false);
+        //选中行数据重置为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //4.导出
+
+  //导出表格数据
+  const exportTable = async () => {
+    if (searchTableFormRef.current) {
+      const formData = new FormData();
+
+      const data = {
+        pageNum: page,
+        pageSize: pageSize,
+        ...searchTableFormRef.current.getFieldsValue(),
+      };
+
+      Object.keys(data).forEach((key) => {
+        if (data[key] !== undefined) {
+          formData.append(key, data[key]);
+        }
+      });
+
+      await fetchFile(
+        exportAPI,
+        push,
+        {
+          method: "POST",
+          body: formData,
+        },
+        `${exportFilePrefix}_${new Date().getTime()}.xlsx`
+      );
+    }
+  };
+
+  //5.选择行
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [selectedRow, setSelectedRow] = useState(undefined as any);
+
+  //修改按钮是否可用,选中行时才可用
+  const [rowCanModify, setRowCanModify] = useState(false);
+
+  //删除按钮是否可用,选中行时才可用
+  const [rowCanDelete, setRowCanDelete] = useState(false);
+
+  //ProTable rowSelection
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[], selectedRows: any[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+
+      if (newSelectedRowKeys && newSelectedRowKeys.length == 1) {
+        setSelectedRow(selectedRows[0]);
+        setRowCanModify(true);
+      } else {
+        setRowCanModify(false);
+        setSelectedRow(undefined);
+      }
+    },
+
+    //复选框的额外禁用判断
+    // getCheckboxProps: (record) => ({
+    //   disabled: record.userId == 1,
+    // }),
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionTableRef = useRef<ActionType>();
+  //搜索表单对象引用
+  const searchTableFormRef = useRef<ProFormInstance>();
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  //刷新缓存
+  const refreshCache = async () => {
+    const body = await fetchApi(refreshAPI, push, {
+      method: "DELETE",
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("刷新成功");
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  return (
+    <PageContainer title={false}>
+      <ProTable
+        formRef={searchTableFormRef}
+        rowKey="configId"
+        rowSelection={{
+          selectedRowKeys,
+          ...rowSelection,
+        }}
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await queryTableData(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+          onChange: pageChange,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionTableRef}
+        toolbar={{
+          actions: [
+            <ModalForm
+              key="addmodal"
+              title="添加参数"
+              trigger={
+                <Button icon={<PlusOutlined />} type="primary">
+                  新建
+                </Button>
+              }
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+              }}
+              submitTimeout={2000}
+              onFinish={executeAddData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="configName"
+                  label="参数名称"
+                  placeholder="请输入参数名称"
+                  rules={[{ required: true, message: "请输入参数名称" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="configKey"
+                  label="参数键名"
+                  placeholder="请输入参数键名"
+                  rules={[{ required: true, message: "请输入参数键名" }]}
+                />
+                <ProFormText
+                  width="md"
+                  name="configValue"
+                  label="参数键值"
+                  placeholder="请输入参数键值"
+                  rules={[{ required: true, message: "请输入参数键值" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormRadio.Group
+                  name="configType"
+                  width="sm"
+                  label="系统内置"
+                  initialValue="Y"
+                  options={[
+                    {
+                      label: "是",
+                      value: "Y",
+                    },
+                    {
+                      label: "否",
+                      value: "N",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProFormTextArea
+                name="remark"
+                width={688}
+                label="备注"
+                placeholder="请输入内容"
+              />
+            </ModalForm>,
+            <ModalForm
+              key="modifymodal"
+              title="修改参数"
+              formRef={modifyFormRef}
+              trigger={
+                <Button
+                  icon={<FontAwesomeIcon icon={faPenToSquare} />}
+                  disabled={!rowCanModify}
+                  onClick={() => onClickShowRowModifyModal()}
+                >
+                  修改
+                </Button>
+              }
+              open={isShowModifyDataModal}
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+                onCancel: () => {
+                  setIsShowModifyDataModal(false);
+                },
+              }}
+              submitTimeout={2000}
+              onFinish={executeModifyData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="configName"
+                  label="参数名称"
+                  placeholder="请输入参数名称"
+                  rules={[{ required: true, message: "请输入参数名称" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="configKey"
+                  label="参数键名"
+                  placeholder="请输入参数键名"
+                  rules={[{ required: true, message: "请输入参数键名" }]}
+                />
+                <ProFormText
+                  width="md"
+                  name="configValue"
+                  label="参数键值"
+                  placeholder="请输入参数键值"
+                  rules={[{ required: true, message: "请输入参数键值" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormRadio.Group
+                  name="configType"
+                  width="sm"
+                  label="系统内置"
+                  initialValue="Y"
+                  options={[
+                    {
+                      label: "是",
+                      value: "Y",
+                    },
+                    {
+                      label: "否",
+                      value: "N",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProFormTextArea
+                name="remark"
+                width={688}
+                label="备注"
+                placeholder="请输入内容"
+              />
+            </ModalForm>,
+
+            <Button
+              key="danger"
+              danger
+              icon={<DeleteOutlined />}
+              disabled={!rowCanDelete}
+              onClick={() => onClickDeleteRow()}
+            >
+              删除
+            </Button>,
+            <Button
+              key="export"
+              type="primary"
+              icon={<FontAwesomeIcon icon={faDownload} />}
+              onClick={exportTable}
+            >
+              导出
+            </Button>,
+            <Button
+              key="refresh"
+              type="primary"
+              icon={<FontAwesomeIcon icon={faRotate} />}
+              onClick={refreshCache}
+            >
+              刷新缓存
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionTableRef.current) {
+                  actionTableRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+    </PageContainer>
+  );
+}

+ 718 - 0
app/(business)/system/dept/page.tsx

@@ -0,0 +1,718 @@
+"use client";
+
+import { fetchApi } from "@/app/_modules/func";
+import {
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  PlusOutlined,
+  ReloadOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import {
+  ModalForm,
+  PageContainer,
+  ProForm,
+  ProFormDigit,
+  ProFormRadio,
+  ProFormText,
+  ProFormTreeSelect,
+  ProTable,
+} from "@ant-design/pro-components";
+import { Button, message, Modal, Space, Tag } from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faArrowsUpDown,
+  faCheck,
+  faPenToSquare,
+  faToggleOff,
+  faToggleOn,
+  faXmark,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useRef, useState } from "react";
+
+//查询表格数据API
+const queryAPI = "/api/system/dept/list";
+//新建数据API
+const newAPI = "/api/system/dept";
+//修改数据API
+const modifyAPI = "/api/system/dept";
+//查询详情数据API
+const queryDetailAPI = "/api/system/dept";
+//删除API
+const deleteAPI = "/api/system/dept";
+
+export default function Dept() {
+  const { push } = useRouter();
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "部门名称",
+      fieldProps: {
+        placeholder: "请输入部门名称",
+      },
+      dataIndex: "deptName",
+      order: 2,
+    },
+    {
+      title: "排序",
+      dataIndex: "orderNum",
+      search: false,
+    },
+    {
+      title: "状态",
+      fieldProps: {
+        placeholder: "请选择部门状态",
+      },
+      dataIndex: "status",
+      valueType: "select",
+      render: (_, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.status === "0" ? "green" : "red"}
+              icon={
+                record.status == 0 ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {_}
+            </Tag>
+          </Space>
+        );
+      },
+      valueEnum: {
+        0: {
+          text: "正常",
+          status: "0",
+        },
+        1: {
+          text: "停用",
+          status: "1",
+        },
+      },
+      order: 1,
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      search: false,
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => {
+        if (record.deptId == 100) {
+          return [
+            <Button
+              key="modifyBtn"
+              type="link"
+              icon={<FontAwesomeIcon icon={faPenToSquare} />}
+              onClick={() => onClickShowRowModifyModal(record)}
+            >
+              修改
+            </Button>,
+            <Button
+              key="newBtn"
+              type="link"
+              icon={<PlusOutlined />}
+              onClick={() => onClickAdd(record)}
+            >
+              新建
+            </Button>,
+          ];
+        } else {
+          return [
+            <Button
+              key="modifyBtn"
+              type="link"
+              icon={<FontAwesomeIcon icon={faPenToSquare} />}
+              onClick={() => onClickShowRowModifyModal(record)}
+            >
+              修改
+            </Button>,
+            <Button
+              key="newBtn"
+              type="link"
+              icon={<PlusOutlined />}
+              onClick={() => onClickAdd(record)}
+            >
+              新建
+            </Button>,
+            <Button
+              key="deleteBtn"
+              type="link"
+              danger
+              icon={<DeleteOutlined />}
+              onClick={() => onClickDeleteRow(record)}
+            >
+              删除
+            </Button>,
+          ];
+        }
+      },
+    },
+  ];
+
+  //0.查询表格数据
+
+  //原始的可展开的所有行的 id
+  const [defaultExpandKeys, setDefaultExpandKeys] = useState<any[]>([]);
+
+  //控制行展开的数据
+  const [expandKeys, setExpandKeys] = useState<any[]>([]);
+
+  const queryTableData = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      ...params,
+    };
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(`${queryAPI}?${queryParams}`, push);
+
+    const root = getRoot(body.data);
+    if (!root) {
+      return body.data;
+    }
+    getChildren(body.data, root);
+    const dataArray = [root];
+
+    const newExpandedKeys: any[] = [];
+    const render = (treeDatas: any[]) => {
+      // 获取到所有可展开的父节点
+      treeDatas.map((item) => {
+        if (item.children) {
+          newExpandedKeys.push(item.deptId);
+          render(item.children);
+        }
+      });
+      return newExpandedKeys;
+    };
+
+    const keys = render(dataArray);
+    setDefaultExpandKeys(keys);
+    setExpandKeys(keys);
+    return dataArray;
+  };
+
+  const getRoot = (data: any[]) => {
+    for (let index = 0; index < data.length; index++) {
+      const item = data[index];
+      if (item.parentId === 0) {
+        return item;
+      }
+    }
+  };
+
+  const getChildren = (data: any[], parentNode: any) => {
+    for (let index = 0; index < data.length; index++) {
+      const item = data[index];
+      if (item.parentId === parentNode.deptId) {
+        parentNode.children.push(item);
+        getChildren(data, item);
+      }
+    }
+
+    if (parentNode.children.length == 0) {
+      delete parentNode.children;
+    }
+  };
+
+  //1.新建
+
+  const [showAddModal, setShowAddModal] = useState(false);
+
+  //新建表单是否带有父节点id
+  const [rowParentId, setRowParentId] = useState(100);
+
+  //点击新建,如果从行点击新建,给定父组织
+  const onClickAdd = (record?: any) => {
+    setRowParentId(record.deptId);
+    setShowAddModal(true);
+  };
+
+  const cancelAddModal = () => {
+    setShowAddModal(false);
+    setRowParentId(100);
+  };
+
+  //确定新建数据
+  const executeAddData = async (values: any) => {
+    const body = await fetchApi(newAPI, push, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        setShowAddModal(false);
+        return true;
+      }
+
+      message.error(body.msg);
+      return false;
+    }
+    return false;
+  };
+
+  //2.修改
+
+  //是否展示修改对话框
+  const [isShowModifyDataModal, setIsShowModifyDataModal] = useState(false);
+
+  //展示修改对话框
+  const onClickShowRowModifyModal = (record: any) => {
+    queryRowData(record);
+    setIsShowModifyDataModal(true);
+  };
+
+  //修改数据表单引用
+  const modifyFormRef = useRef<ProFormInstance>();
+
+  //操作当前数据的附加数据
+  const [operatRowData, setOperateRowData] = useState<{
+    [key: string]: any;
+  }>({});
+
+  //查询并加载待修改数据的详细信息
+  const queryRowData = async (record: any) => {
+    const deptId = record.deptId;
+
+    operatRowData["deptId"] = deptId;
+    operatRowData["ancestors"] = record.ancestors;
+
+    setOperateRowData(operatRowData);
+
+    if (deptId !== undefined) {
+      const body = await fetchApi(`${queryDetailAPI}/${deptId}`, push);
+
+      if (body !== undefined) {
+        if (body.code == 200) {
+          console.log("modi:", modifyFormRef);
+          modifyFormRef?.current?.setFieldsValue({
+            //需要加载到修改表单中的数据
+            parentId: body.data.parentId,
+            deptName: body.data.deptName,
+            orderNum: body.data.orderNum,
+            leader: body.data.leader,
+            phone: body.data.phone,
+            email: body.data.email,
+            status: body.data.status,
+          });
+        }
+      }
+    }
+  };
+
+  //确认修改数据
+  const executeModifyData = async (values: any) => {
+    values["deptId"] = operatRowData["deptId"];
+    values["ancestors"] = operatRowData["ancestors"];
+
+    const body = await fetchApi(modifyAPI, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        setIsShowModifyDataModal(false);
+        return true;
+      }
+      message.error(body.msg);
+      return false;
+    }
+  };
+
+  //3.展开/折叠
+
+  //点击展开/折叠按钮
+  const onClickExpandRow = () => {
+    if (expandKeys.length > 0) {
+      setExpandKeys([]);
+    } else {
+      setExpandKeys(defaultExpandKeys);
+    }
+  };
+
+  //处理行的展开/折叠逻辑
+  const handleExpand = (expanded: boolean, record: any) => {
+    let keys = [...expandKeys];
+
+    if (expanded) {
+      keys.push(record.deptId);
+    } else {
+      keys = keys.filter((key: number) => key !== record.deptId);
+    }
+
+    setExpandKeys(keys);
+  };
+
+  //4.导出
+
+  //5.选择行
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionTableRef = useRef<ActionType>();
+  //搜索表单对象引用
+  const searchTableFormRef = useRef<ProFormInstance>();
+
+  const getDeptList = async () => {
+    const body = await fetchApi(queryAPI, push);
+    if (body !== undefined) {
+      const root = getRoot(body.data);
+      if (!root) {
+        return body.data;
+      }
+      getChildren(body.data, root);
+      return [root];
+    }
+  };
+
+  //点击删除按钮
+  const onClickDeleteRow = (record: any) => {
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定删除部门名称为“${record.deptName}”的数据项?`,
+      onOk() {
+        executeDeleteRow(record.deptId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的部门
+  const executeDeleteRow = async (roleId: any) => {
+    const body = await fetchApi(`${deleteAPI}/${roleId}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  return (
+    <PageContainer title={false}>
+      <ProTable
+        formRef={searchTableFormRef}
+        rowKey="deptId"
+        columns={columns}
+        expandable={{
+          expandedRowKeys: expandKeys,
+          onExpand: handleExpand,
+        }}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await queryTableData(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data,
+              success: true,
+              total: data.length,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={false}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionTableRef}
+        toolbar={{
+          actions: [
+            <ModalForm
+              key="addmodal"
+              title="添加部门"
+              open={showAddModal}
+              trigger={
+                <Button icon={<PlusOutlined />} type="primary">
+                  新建
+                </Button>
+              }
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+                onCancel: () => {
+                  cancelAddModal();
+                },
+              }}
+              submitTimeout={2000}
+              onFinish={executeAddData}
+            >
+              <ProForm.Group>
+                <ProFormTreeSelect
+                  width="md"
+                  name="parentId"
+                  initialValue={rowParentId}
+                  label="上级部门"
+                  placeholder="请选择上级部门"
+                  rules={[{ required: true, message: "请选择上级部门" }]}
+                  request={getDeptList}
+                  fieldProps={{
+                    filterTreeNode: true,
+                    showSearch: true,
+                    treeNodeFilterProp: "label",
+                    fieldNames: {
+                      label: "deptName",
+                      value: "deptId",
+                    },
+                  }}
+                />
+                <ProFormText
+                  width="md"
+                  name="deptName"
+                  label="部门名称"
+                  placeholder="请输入部门名称"
+                  rules={[{ required: true, message: "请输入部门名称" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormDigit
+                  fieldProps={{ precision: 0 }}
+                  width="md"
+                  name="orderNum"
+                  initialValue="0"
+                  label="排序"
+                  placeholder="请输入排序"
+                  rules={[{ required: true, message: "请输入排序" }]}
+                />
+                <ProFormRadio.Group
+                  name="status"
+                  width="sm"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "停用",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="leader"
+                  label="负责人"
+                  placeholder="请输入负责人"
+                />
+                <ProFormText
+                  width="md"
+                  name="phone"
+                  label="联系电话"
+                  placeholder="请输入联系电话"
+                  rules={[
+                    {
+                      pattern: /^1\d{10}$/,
+                      message: "请输入正确的手机号码",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="email"
+                  label="联系邮箱"
+                  placeholder="请输入联系邮箱"
+                  rules={[{ type: "email", message: "请输入正确的邮箱地址" }]}
+                />
+              </ProForm.Group>
+            </ModalForm>,
+            <Button
+              key="expand"
+              icon={<FontAwesomeIcon icon={faArrowsUpDown} />}
+              onClick={() => onClickExpandRow()}
+            >
+              折叠/展开
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionTableRef.current) {
+                  actionTableRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+      <ModalForm
+        key="modifymodal"
+        title="修改部门"
+        formRef={modifyFormRef}
+        open={isShowModifyDataModal}
+        autoFocusFirstInput
+        modalProps={{
+          destroyOnHidden: true,
+          onCancel: () => {
+            setIsShowModifyDataModal(false);
+          },
+        }}
+        submitTimeout={2000}
+        onFinish={executeModifyData}
+      >
+        <ProForm.Group>
+          <ProFormTreeSelect
+            width="md"
+            name="parentId"
+            label="上级部门"
+            placeholder="请选择上级部门"
+            rules={[{ required: true, message: "请选择上级部门" }]}
+            request={getDeptList}
+            fieldProps={{
+              filterTreeNode: true,
+              showSearch: true,
+              treeNodeFilterProp: "label",
+              fieldNames: {
+                label: "deptName",
+                value: "deptId",
+              },
+            }}
+          />
+          <ProFormText
+            width="md"
+            name="deptName"
+            label="部门名称"
+            placeholder="请输入部门名称"
+            rules={[{ required: true, message: "请输入部门名称" }]}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormDigit
+            fieldProps={{ precision: 0 }}
+            width="md"
+            name="orderNum"
+            initialValue="0"
+            label="排序"
+            placeholder="请输入排序"
+            rules={[{ required: true, message: "请输入排序" }]}
+          />
+          <ProFormRadio.Group
+            name="status"
+            width="sm"
+            label="状态"
+            initialValue="0"
+            options={[
+              {
+                label: "正常",
+                value: "0",
+              },
+              {
+                label: "停用",
+                value: "1",
+              },
+            ]}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormText
+            width="md"
+            name="leader"
+            label="负责人"
+            placeholder="请输入负责人"
+          />
+          <ProFormText
+            width="md"
+            name="phone"
+            label="联系电话"
+            placeholder="请输入联系电话"
+            rules={[
+              {
+                pattern: /^1\d{10}$/,
+                message: "请输入正确的手机号码",
+              },
+            ]}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormText
+            width="md"
+            name="email"
+            label="联系邮箱"
+            placeholder="请输入联系邮箱"
+            rules={[{ type: "email", message: "请输入正确的邮箱地址" }]}
+          />
+        </ProForm.Group>
+      </ModalForm>
+      ,
+    </PageContainer>
+  );
+}

+ 818 - 0
app/(business)/system/dict-data/index/[dictid]/page.tsx

@@ -0,0 +1,818 @@
+"use client";
+
+import { fetchApi, fetchFile } from "@/app/_modules/func";
+import {
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  PlusOutlined,
+  ReloadOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import {
+  ModalForm,
+  PageContainer,
+  ProForm,
+  ProFormDigit,
+  ProFormRadio,
+  ProFormSelect,
+  ProFormText,
+  ProFormTextArea,
+  ProTable,
+} from "@ant-design/pro-components";
+import { Button, message, Modal, Space, Tag } from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faCheck,
+  faDownload,
+  faPenToSquare,
+  faToggleOff,
+  faToggleOn,
+  faXmark,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useRef, useState } from "react";
+
+//查询类型详情
+const queryTypeAPI = "/api/system/dict/type";
+//查询所有类型列表
+const queryTypeListAPI = "/api/system/dict/type/optionselect";
+//查询表格数据API
+const queryAPI = "/api/system/dict/data/list";
+//新建数据API
+const newAPI = "/api/system/dict/data";
+//修改数据API
+const modifyAPI = "/api/system/dict/data";
+//查询详情数据API
+const queryDetailAPI = "/api/system/dict/data";
+//删除API
+const deleteAPI = "/api/system/dict/data";
+//导出API
+const exportAPI = "/api/system/dict/data/export";
+//导出文件前缀名
+const exportFilePrefix = "data";
+
+export default function DictData({ params }: { params: { dictid: string } }) {
+  const { push } = useRouter();
+
+  const [defaultType, setDefaultType] = useState("");
+
+  //获取对应的字典类型的值
+  const getTypeData = async () => {
+    const resp = await fetchApi(`${queryTypeAPI}/${params.dictid}`, push);
+    if (resp != undefined) {
+      if (searchTableFormRef.current) {
+        searchTableFormRef.current.setFieldsValue({
+          dictType: resp.data.dictType,
+        });
+      }
+
+      setDefaultType(resp.data.dictType);
+      return resp.data.dictType;
+    }
+
+    return "";
+  };
+
+  //查询字典类型列表
+  const getTypeList = async () => {
+    const dataArray: Array<any> = new Array<any>();
+    const resp = await fetchApi(queryTypeListAPI, push);
+    if (resp != undefined) {
+      resp.data.forEach((item: any) => {
+        const type = {
+          label: item.dictName,
+          value: item.dictType,
+        };
+        dataArray.push(type);
+      });
+    }
+
+    return dataArray;
+  };
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "字典名称",
+      dataIndex: "dictType",
+      valueType: "select",
+      fieldProps: {
+        allowClear: false,
+      },
+      request: getTypeList,
+      hideInTable: true,
+      order: 3,
+    },
+    {
+      title: "数据编码",
+      dataIndex: "dictCode",
+      search: false,
+    },
+    {
+      title: "数据标签",
+      fieldProps: {
+        placeholder: "请输入数据标签",
+      },
+      dataIndex: "dictLabel",
+      order: 2,
+      render: (_, record) => {
+        const isTag = record.listClass === "";
+        let tagColor = "default";
+        if (record.listClass === "") {
+          return _;
+        } else {
+          switch (record.listClass) {
+            case "default":
+              tagColor = "processing";
+              break;
+            case "primary":
+              tagColor = "processing";
+              break;
+            case "success":
+              tagColor = "success";
+              break;
+            case "info":
+              tagColor = "default";
+              break;
+            case "warning":
+              tagColor = "warning";
+              break;
+            case "danger":
+              tagColor = "error";
+              break;
+            default:
+              tagColor = "processing";
+              break;
+          }
+          return (
+            <Space>
+              <Tag color={tagColor}>{_}</Tag>
+            </Space>
+          );
+        }
+      },
+    },
+    {
+      title: "数据键值",
+      dataIndex: "dictValue",
+      search: false,
+    },
+    {
+      title: "数据排序",
+      dataIndex: "dictSort",
+      sorter: true,
+      search: false,
+    },
+    {
+      title: "状态",
+      fieldProps: {
+        placeholder: "请选择数据状态",
+      },
+      dataIndex: "status",
+      valueType: "select",
+      render: (_, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.status === "0" ? "green" : "red"}
+              icon={
+                record.status == 0 ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {_}
+            </Tag>
+          </Space>
+        );
+      },
+      valueEnum: {
+        0: {
+          text: "正常",
+          status: "0",
+        },
+        1: {
+          text: "停用",
+          status: "1",
+        },
+      },
+      order: 1,
+    },
+    {
+      title: "备注",
+      dataIndex: "remark",
+      search: false,
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      search: false,
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => [
+        <Button
+          key="modifyBtn"
+          type="link"
+          icon={<FontAwesomeIcon icon={faPenToSquare} />}
+          onClick={() => onClickShowRowModifyModal(record)}
+        >
+          修改
+        </Button>,
+        <Button
+          key="deleteBtn"
+          type="link"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => onClickDeleteRow(record)}
+        >
+          删除
+        </Button>,
+      ],
+    },
+  ];
+
+  //0.查询表格数据
+  const queryTableData = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    //如果没有带上默认的字典类型,查询绑定上
+    if (!("dictType" in searchParams)) {
+      const defaultType = await getTypeData();
+      queryParams.append("dictType", defaultType);
+    }
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(`${queryAPI}?${queryParams}`, push);
+
+    return body;
+  };
+
+  //1.新建
+
+  //确定新建数据
+  const executeAddData = async (values: any) => {
+    const body = await fetchApi(newAPI, push, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        return true;
+      }
+
+      message.error(body.msg);
+      return false;
+    }
+    return false;
+  };
+
+  //2.修改
+
+  //是否展示修改对话框
+  const [isShowModifyDataModal, setIsShowModifyDataModal] = useState(false);
+
+  //展示修改对话框
+  const onClickShowRowModifyModal = (record?: any) => {
+    queryRowData(record);
+    setIsShowModifyDataModal(true);
+  };
+
+  //修改数据表单引用
+  const modifyFormRef = useRef<ProFormInstance>();
+
+  //操作当前数据的附加数据
+  const [operatRowData, setOperateRowData] = useState<{
+    [key: string]: any;
+  }>({});
+
+  //查询并加载待修改数据的详细信息
+  const queryRowData = async (record?: any) => {
+    const dictCode =
+      record !== undefined ? record.dictCode : selectedRow.dictCode;
+
+    operatRowData["dictCode"] = dictCode;
+
+    setOperateRowData(operatRowData);
+
+    if (dictCode !== undefined) {
+      const body = await fetchApi(`${queryDetailAPI}/${dictCode}`, push);
+
+      if (body !== undefined) {
+        if (body.code == 200) {
+          modifyFormRef?.current?.setFieldsValue({
+            //需要加载到修改表单中的数据
+            dictType: body.data.dictType,
+            dictLabel: body.data.dictLabel,
+            dictValue: body.data.dictValue,
+            dictSort: body.data.dictSort,
+            status: body.data.status,
+            listClass: body.data.listClass,
+            cssClass: body.data.cssClass,
+            remark: body.data.remark,
+          });
+        }
+      }
+    }
+  };
+
+  //确认修改数据
+  const executeModifyData = async (values: any) => {
+    values["dictCode"] = operatRowData["dictCode"];
+
+    const body = await fetchApi(modifyAPI, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        setIsShowModifyDataModal(false);
+        return true;
+      }
+      message.error(body.msg);
+      return false;
+    }
+  };
+
+  //3.删除
+
+  //点击删除按钮,展示删除确认框
+  const onClickDeleteRow = (record?: any) => {
+    const dictCode =
+      record != undefined ? record.dictCode : selectedRowKeys.join(",");
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定删除字典编码为“${dictCode}”的数据项?`,
+      onOk() {
+        executeDeleteRow(dictCode);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的数据
+  const executeDeleteRow = async (dictCode: any) => {
+    const body = await fetchApi(`${deleteAPI}/${dictCode}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+
+        //修改按钮变回不可点击
+        setRowCanModify(false);
+        //删除按钮变回不可点击
+        setRowCanDelete(false);
+        //选中行数据重置为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //4.导出
+
+  //导出表格数据
+  const exportTable = async () => {
+    if (searchTableFormRef.current) {
+      const formData = new FormData();
+
+      const data = {
+        pageNum: page,
+        pageSize: pageSize,
+        ...searchTableFormRef.current.getFieldsValue(),
+      };
+
+      Object.keys(data).forEach((key) => {
+        if (data[key] !== undefined) {
+          formData.append(key, data[key]);
+        }
+      });
+
+      await fetchFile(
+        exportAPI,
+        push,
+        {
+          method: "POST",
+          body: formData,
+        },
+        `${exportFilePrefix}_${new Date().getTime()}.xlsx`
+      );
+    }
+  };
+
+  //5.选择行
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [selectedRow, setSelectedRow] = useState(undefined as any);
+
+  //修改按钮是否可用,选中行时才可用
+  const [rowCanModify, setRowCanModify] = useState(false);
+
+  //删除按钮是否可用,选中行时才可用
+  const [rowCanDelete, setRowCanDelete] = useState(false);
+
+  //ProTable rowSelection
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[], selectedRows: any[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+
+      if (newSelectedRowKeys && newSelectedRowKeys.length == 1) {
+        setSelectedRow(selectedRows[0]);
+        setRowCanModify(true);
+      } else {
+        setRowCanModify(false);
+        setSelectedRow(undefined);
+      }
+    },
+
+    //复选框的额外禁用判断
+    // getCheckboxProps: (record) => ({
+    //   disabled: record.userId == 1,
+    // }),
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionTableRef = useRef<ActionType>();
+  //搜索表单对象引用
+  const searchTableFormRef = useRef<ProFormInstance>();
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  return (
+    <PageContainer
+      header={{
+        title: "字典数据",
+        onBack(e) {
+          push("/system/dict");
+        },
+      }}
+    >
+      <ProTable
+        formRef={searchTableFormRef}
+        rowKey="dictCode"
+        rowSelection={{
+          selectedRowKeys,
+          ...rowSelection,
+        }}
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await queryTableData(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+          onChange: pageChange,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionTableRef}
+        toolbar={{
+          actions: [
+            <ModalForm
+              key="addmodal"
+              title="添加字典数据"
+              trigger={
+                <Button icon={<PlusOutlined />} type="primary">
+                  新建
+                </Button>
+              }
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+              }}
+              submitTimeout={2000}
+              onFinish={executeAddData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="dictType"
+                  label="字典类型"
+                  initialValue={defaultType}
+                  disabled
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="dictLabel"
+                  label="数据标签"
+                  rules={[{ required: true, message: "请输入数据标签" }]}
+                />
+                <ProFormText
+                  width="md"
+                  name="dictValue"
+                  label="数据键值"
+                  rules={[{ required: true, message: "请输入数据键值" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormDigit
+                  fieldProps={{ precision: 0 }}
+                  width="md"
+                  name="dictSort"
+                  initialValue="0"
+                  label="数据排序"
+                  placeholder="请输入数据排序"
+                  rules={[{ required: true, message: "请输入数据排序" }]}
+                />
+                <ProFormRadio.Group
+                  width="md"
+                  name="status"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "停用",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormSelect
+                  width="md"
+                  name="listClass"
+                  label="回显样式"
+                  valueEnum={{
+                    default: {
+                      text: "默认(default)",
+                      status: "default",
+                    },
+                    primary: {
+                      text: "主要(primary)",
+                      status: "primary",
+                    },
+                    success: {
+                      text: "成功(成功)",
+                      status: "success",
+                    },
+                    info: {
+                      text: "信息(info)",
+                      status: "info",
+                    },
+                    warning: {
+                      text: "警告(warning)",
+                      status: "warning",
+                    },
+                    danger: {
+                      text: "危险(danger)",
+                      status: "danger",
+                    },
+                  }}
+                />
+                <ProFormText width="md" name="cssClass" label="样式属性" />
+              </ProForm.Group>
+              <ProFormTextArea
+                name="remark"
+                width={688}
+                label="备注"
+                placeholder="请输入内容"
+              />
+            </ModalForm>,
+            <ModalForm
+              key="modifymodal"
+              title="修改岗位"
+              formRef={modifyFormRef}
+              trigger={
+                <Button
+                  icon={<FontAwesomeIcon icon={faPenToSquare} />}
+                  disabled={!rowCanModify}
+                  onClick={() => onClickShowRowModifyModal()}
+                >
+                  修改
+                </Button>
+              }
+              open={isShowModifyDataModal}
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+                onCancel: () => {
+                  setIsShowModifyDataModal(false);
+                },
+              }}
+              submitTimeout={2000}
+              onFinish={executeModifyData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="dictType"
+                  label="字典类型"
+                  disabled
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="dictLabel"
+                  label="字典标签"
+                  rules={[{ required: true, message: "请输入字典标签" }]}
+                />
+                <ProFormText
+                  width="md"
+                  name="dictValue"
+                  label="字典键值"
+                  rules={[{ required: true, message: "请输入字典键值" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormDigit
+                  fieldProps={{ precision: 0 }}
+                  width="md"
+                  name="dictSort"
+                  initialValue="0"
+                  label="显示排序"
+                  placeholder="请输入显示排序"
+                  rules={[{ required: true, message: "请输入显示排序" }]}
+                />
+                <ProFormRadio.Group
+                  width="md"
+                  name="status"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "停用",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormSelect
+                  width="md"
+                  name="listClass"
+                  label="回显样式"
+                  valueEnum={{
+                    default: {
+                      text: "默认(default)",
+                      status: "default",
+                    },
+                    primary: {
+                      text: "主要(primary)",
+                      status: "primary",
+                    },
+                    success: {
+                      text: "成功(成功)",
+                      status: "success",
+                    },
+                    info: {
+                      text: "信息(info)",
+                      status: "info",
+                    },
+                    warning: {
+                      text: "警告(warning)",
+                      status: "warning",
+                    },
+                    danger: {
+                      text: "危险(danger)",
+                      status: "danger",
+                    },
+                  }}
+                />
+                <ProFormText width="md" name="cssClass" label="样式属性" />
+              </ProForm.Group>
+              <ProFormTextArea
+                name="remark"
+                width={688}
+                label="备注"
+                placeholder="请输入内容"
+              />
+            </ModalForm>,
+
+            <Button
+              key="danger"
+              danger
+              icon={<DeleteOutlined />}
+              disabled={!rowCanDelete}
+              onClick={() => onClickDeleteRow()}
+            >
+              删除
+            </Button>,
+            <Button
+              key="export"
+              type="primary"
+              icon={<FontAwesomeIcon icon={faDownload} />}
+              onClick={exportTable}
+            >
+              导出
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionTableRef.current) {
+                  actionTableRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+    </PageContainer>
+  );
+}

+ 667 - 0
app/(business)/system/dict/page.tsx

@@ -0,0 +1,667 @@
+"use client";
+
+import { fetchApi, fetchFile } from "@/app/_modules/func";
+import {
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  PlusOutlined,
+  ReloadOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import {
+  ModalForm,
+  PageContainer,
+  ProForm,
+  ProFormRadio,
+  ProFormText,
+  ProFormTextArea,
+  ProTable,
+} from "@ant-design/pro-components";
+import { Button, message, Modal, Space, Tag } from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faCheck,
+  faDownload,
+  faPenToSquare,
+  faRotate,
+  faToggleOff,
+  faToggleOn,
+  faXmark,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useRef, useState } from "react";
+
+//查询表格数据API
+const queryAPI = "/api/system/dict/type/list";
+//新建数据API
+const newAPI = "/api/system/dict/type";
+//修改数据API
+const modifyAPI = "/api/system/dict/type";
+//查询详情数据API
+const queryDetailAPI = "/api/system/dict/type";
+//删除API
+const deleteAPI = "/api/system/dict/type";
+//导出API
+const exportAPI = "/api/system/dict/type/export";
+//导出文件前缀名
+const exportFilePrefix = "dict";
+//刷新缓存
+const refreshAPI = "/api/system/dict/type/refreshCache";
+
+export default function Dict() {
+  const { push } = useRouter();
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "字典编号",
+      dataIndex: "dictId",
+      search: false,
+    },
+    {
+      title: "字典名称",
+      fieldProps: {
+        placeholder: "请输入字典名称",
+      },
+      dataIndex: "dictName",
+      ellipsis: true,
+      sorter: true,
+      order: 4,
+    },
+    {
+      title: "字典类型",
+      fieldProps: {
+        placeholder: "请输入字典类型",
+      },
+      dataIndex: "dictType",
+      ellipsis: true,
+      order: 3,
+      render: (_, record) => {
+        return (
+          <a onClick={() => push(`/system/dict-data/index/${record.dictId}`)}>
+            {record.dictType}
+          </a>
+        );
+      },
+    },
+    {
+      title: "状态",
+      fieldProps: {
+        placeholder: "请选择字典状态",
+      },
+      dataIndex: "status",
+      valueType: "select",
+      render: (_, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.status === "0" ? "green" : "red"}
+              icon={
+                record.status == 0 ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {_}
+            </Tag>
+          </Space>
+        );
+      },
+      valueEnum: {
+        0: {
+          text: "正常",
+          status: "0",
+        },
+        1: {
+          text: "停用",
+          status: "1",
+        },
+      },
+      order: 2,
+    },
+    {
+      title: "备注",
+      dataIndex: "remark",
+      ellipsis: true,
+      search: false,
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      search: false,
+    },
+    {
+      title: "创建时间",
+      fieldProps: {
+        placeholder: ["开始日期", "结束日期"],
+      },
+      dataIndex: "createTimeRange",
+      valueType: "dateRange",
+      hideInTable: true,
+      order: 1,
+      search: {
+        transform: (value) => {
+          return {
+            "params[beginTime]": `${value[0]} 00:00:00`,
+            "params[endTime]": `${value[1]} 23:59:59`,
+          };
+        },
+      },
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => [
+        <Button
+          key="modifyBtn"
+          type="link"
+          icon={<FontAwesomeIcon icon={faPenToSquare} />}
+          onClick={() => onClickShowRowModifyModal(record)}
+        >
+          修改
+        </Button>,
+        <Button
+          key="deleteBtn"
+          type="link"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => onClickDeleteRow(record)}
+        >
+          删除
+        </Button>,
+      ],
+    },
+  ];
+
+  //0.查询表格数据
+  const queryTableData = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(`${queryAPI}?${queryParams}`, push);
+
+    return body;
+  };
+
+  //1.新建
+
+  //确定新建数据
+  const executeAddData = async (values: any) => {
+    const body = await fetchApi(newAPI, push, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        return true;
+      }
+
+      message.error(body.msg);
+      return false;
+    }
+    return false;
+  };
+
+  //2.修改
+
+  //是否展示修改对话框
+  const [isShowModifyDataModal, setIsShowModifyDataModal] = useState(false);
+
+  //展示修改对话框
+  const onClickShowRowModifyModal = (record?: any) => {
+    queryRowData(record);
+    setIsShowModifyDataModal(true);
+  };
+
+  //修改数据表单引用
+  const modifyFormRef = useRef<ProFormInstance>();
+
+  //操作当前数据的附加数据
+  const [operatRowData, setOperateRowData] = useState<{
+    [key: string]: any;
+  }>({});
+
+  //查询并加载待修改数据的详细信息
+  const queryRowData = async (record?: any) => {
+    const dictId = record !== undefined ? record.dictId : selectedRow.dictId;
+
+    operatRowData["dictId"] = dictId;
+
+    setOperateRowData(operatRowData);
+
+    if (dictId !== undefined) {
+      const body = await fetchApi(`${queryDetailAPI}/${dictId}`, push);
+
+      if (body !== undefined) {
+        if (body.code == 200) {
+          modifyFormRef?.current?.setFieldsValue({
+            //需要加载到修改表单中的数据
+            dictName: body.data.dictName,
+            dictType: body.data.dictType,
+            status: body.data.status,
+            remark: body.data.remark,
+          });
+        }
+      }
+    }
+  };
+
+  //确认修改数据
+  const executeModifyData = async (values: any) => {
+    values["dictId"] = operatRowData["dictId"];
+
+    const body = await fetchApi(modifyAPI, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        setIsShowModifyDataModal(false);
+        return true;
+      }
+      message.error(body.msg);
+      return false;
+    }
+  };
+
+  //3.删除
+
+  //点击删除按钮,展示删除确认框
+  const onClickDeleteRow = (record?: any) => {
+    const dictId =
+      record != undefined ? record.dictId : selectedRowKeys.join(",");
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `是否确认删除字典编号为“${dictId}”的数据项?`,
+      onOk() {
+        executeDeleteRow(dictId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的数据
+  const executeDeleteRow = async (dictId: any) => {
+    const body = await fetchApi(`${deleteAPI}/${dictId}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+
+        //修改按钮变回不可点击
+        setRowCanModify(false);
+        //删除按钮变回不可点击
+        setRowCanDelete(false);
+        //选中行数据重置为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //4.导出
+
+  //导出表格数据
+  const exportTable = async () => {
+    if (searchTableFormRef.current) {
+      const formData = new FormData();
+
+      const data = {
+        pageNum: page,
+        pageSize: pageSize,
+        ...searchTableFormRef.current.getFieldsValue(),
+      };
+
+      Object.keys(data).forEach((key) => {
+        if (data[key] !== undefined) {
+          formData.append(key, data[key]);
+        }
+      });
+
+      await fetchFile(
+        exportAPI,
+        push,
+        {
+          method: "POST",
+          body: formData,
+        },
+        `${exportFilePrefix}_${new Date().getTime()}.xlsx`
+      );
+    }
+  };
+
+  //5.选择行
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [selectedRow, setSelectedRow] = useState(undefined as any);
+
+  //修改按钮是否可用,选中行时才可用
+  const [rowCanModify, setRowCanModify] = useState(false);
+
+  //删除按钮是否可用,选中行时才可用
+  const [rowCanDelete, setRowCanDelete] = useState(false);
+
+  //ProTable rowSelection
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[], selectedRows: any[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+
+      if (newSelectedRowKeys && newSelectedRowKeys.length == 1) {
+        setSelectedRow(selectedRows[0]);
+        setRowCanModify(true);
+      } else {
+        setRowCanModify(false);
+        setSelectedRow(undefined);
+      }
+    },
+
+    //复选框的额外禁用判断
+    // getCheckboxProps: (record) => ({
+    //   disabled: record.userId == 1,
+    // }),
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionTableRef = useRef<ActionType>();
+  //搜索表单对象引用
+  const searchTableFormRef = useRef<ProFormInstance>();
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  //刷新缓存
+  const refreshCache = async () => {
+    const body = await fetchApi(refreshAPI, push, {
+      method: "DELETE",
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("刷新成功");
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  return (
+    <PageContainer title={false}>
+      <ProTable
+        formRef={searchTableFormRef}
+        rowKey="dictId"
+        rowSelection={{
+          selectedRowKeys,
+          ...rowSelection,
+        }}
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await queryTableData(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+          onChange: pageChange,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionTableRef}
+        toolbar={{
+          actions: [
+            <ModalForm
+              key="addmodal"
+              title="添加字典类型"
+              trigger={
+                <Button icon={<PlusOutlined />} type="primary">
+                  新建
+                </Button>
+              }
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+              }}
+              submitTimeout={2000}
+              onFinish={executeAddData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="dictName"
+                  label="字典名称"
+                  placeholder="请输入字典名称"
+                  rules={[{ required: true, message: "请输入字典名称" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="dictType"
+                  label="字典类型"
+                  placeholder="请输入字典类型"
+                  rules={[{ required: true, message: "请输入字典类型" }]}
+                />
+                <ProFormRadio.Group
+                  name="status"
+                  width="sm"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "停用",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProFormTextArea
+                name="remark"
+                width={688}
+                label="备注"
+                placeholder="请输入内容"
+              />
+            </ModalForm>,
+            <ModalForm
+              key="modifymodal"
+              title="修改字典类型"
+              formRef={modifyFormRef}
+              trigger={
+                <Button
+                  icon={<FontAwesomeIcon icon={faPenToSquare} />}
+                  disabled={!rowCanModify}
+                  onClick={() => onClickShowRowModifyModal()}
+                >
+                  修改
+                </Button>
+              }
+              open={isShowModifyDataModal}
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+                onCancel: () => {
+                  setIsShowModifyDataModal(false);
+                },
+              }}
+              submitTimeout={2000}
+              onFinish={executeModifyData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="dictName"
+                  label="字典名称"
+                  placeholder="请输入字典名称"
+                  rules={[{ required: true, message: "请输入字典名称" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="dictType"
+                  label="字典类型"
+                  placeholder="请输入字典类型"
+                  rules={[{ required: true, message: "请输入字典类型" }]}
+                />
+                <ProFormRadio.Group
+                  name="status"
+                  width="sm"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "停用",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProFormTextArea
+                name="remark"
+                width={688}
+                label="备注"
+                placeholder="请输入内容"
+              />
+            </ModalForm>,
+
+            <Button
+              key="danger"
+              danger
+              icon={<DeleteOutlined />}
+              disabled={!rowCanDelete}
+              onClick={() => onClickDeleteRow()}
+            >
+              删除
+            </Button>,
+            <Button
+              key="export"
+              type="primary"
+              icon={<FontAwesomeIcon icon={faDownload} />}
+              onClick={exportTable}
+            >
+              导出
+            </Button>,
+            <Button
+              key="refresh"
+              type="primary"
+              icon={<FontAwesomeIcon icon={faRotate} />}
+              onClick={refreshCache}
+            >
+              刷新缓存
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionTableRef.current) {
+                  actionTableRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+    </PageContainer>
+  );
+}

+ 461 - 0
app/(business)/system/log/logininfor/page.tsx

@@ -0,0 +1,461 @@
+"use client";
+
+import { fetchApi, fetchFile } from "@/app/_modules/func";
+import {
+  ClearOutlined,
+  DeleteOutlined,
+  EyeOutlined,
+  ImportOutlined,
+  ReloadOutlined,
+  ExclamationCircleFilled,
+  UnlockOutlined,
+} from "@ant-design/icons";
+import type {
+  ProColumns,
+  ProFormInstance,
+  ActionType,
+} from "@ant-design/pro-components";
+import {
+  PageContainer,
+  ProDescriptions,
+  ProTable,
+} from "@ant-design/pro-components";
+import { Button, Modal, Space, Tag, message } from "antd";
+import { useRouter } from "next/navigation";
+import {
+  faCheck,
+  faToggleOff,
+  faToggleOn,
+  faXmark,
+  faDownload,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useRef, useState } from "react";
+
+export default function OperLog() {
+  const { push } = useRouter();
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "访问编号",
+      dataIndex: "infoId",
+      search: false,
+    },
+    {
+      title: "用户名称",
+      fieldProps: {
+        placeholder: "请输入用户名称",
+      },
+      dataIndex: "userName",
+      order: 3,
+      sorter: true,
+    },
+    {
+      title: "登录地址",
+      fieldProps: {
+        placeholder: "请输入登录地址",
+      },
+      dataIndex: "ipaddr",
+      order: 4,
+    },
+    {
+      title: "登录地点",
+      dataIndex: "loginLocation",
+      search: false,
+    },
+
+    {
+      title: "浏览器",
+      dataIndex: "browser",
+      search: false,
+    },
+    {
+      title: "操作系统",
+      dataIndex: "os",
+      search: false,
+    },
+    {
+      title: "登录状态",
+      fieldProps: {
+        placeholder: "请选择登录状态",
+      },
+      dataIndex: "status",
+      valueType: "select",
+      order: 2,
+      render: (_, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.status == 0 ? "green" : "red"}
+              icon={
+                record.status == 0 ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {_}
+            </Tag>
+          </Space>
+        );
+      },
+      valueEnum: {
+        0: {
+          text: "成功",
+          status: "0",
+        },
+        1: {
+          text: "失败",
+          status: "1",
+        },
+      },
+    },
+    {
+      title: "操作信息",
+      dataIndex: "msg",
+      search: false,
+    },
+    {
+      title: "登录时间",
+      dataIndex: "loginTime",
+      valueType: "dateTime",
+      search: false,
+      sorter: true,
+    },
+    {
+      title: "操作时间",
+      fieldProps: {
+        placeholder: ["开始日期", "结束日期"],
+      },
+      dataIndex: "loginTimeRange",
+      valueType: "dateRange",
+      hideInTable: true,
+      order: 1,
+      search: {
+        transform: (value) => {
+          return {
+            "params[beginTime]": `${value[0]} 00:00:00`,
+            "params[endTime]": `${value[1]} 23:59:59`,
+          };
+        },
+      },
+    },
+  ];
+
+  //查询日志数据
+  const getLog = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(
+      `/api/monitor/logininfor/list?${queryParams}`,
+      push
+    );
+    console.log("data:", body);
+    return body;
+  };
+
+  //删除按钮是否可用,选中行时才可用
+  const [rowCanDelete, setRowCanDelete] = useState(false);
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [selectedRow, setSelectedRow] = useState(undefined as any);
+
+  //解锁按钮是否可用
+  const [rowCanUnlock, setRowCanUnlock] = useState(false);
+
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[], selectedRows: any[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+
+      if (newSelectedRowKeys && newSelectedRowKeys.length == 1) {
+        console.log("row:", selectedRows[0]);
+        setSelectedRow(selectedRows[0]);
+        setRowCanUnlock(true);
+      } else {
+        setRowCanUnlock(false);
+        setSelectedRow(undefined);
+      }
+    },
+  };
+
+  //点击删除按钮
+  const onClickDeleteRow = () => {
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定删除访问编号为“${selectedRowKeys.join(",")}”的数据项?`,
+      onOk() {
+        executeDeleteRow();
+      },
+      onCancel() {},
+    });
+  };
+
+  //点击清空按钮
+  const onClickClear = () => {
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: "是否确认清空所有操作日志数据项?",
+      onOk() {
+        executeClear();
+      },
+      onCancel() {},
+    });
+  };
+
+  //点击解锁按钮
+  const onClickUnlock = () => {
+    console.log("row:", selectedRow);
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定解锁用户"${selectedRow.userName}"数据项?`,
+      onOk() {
+        executeUnlock(selectedRow.userName);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的日志数据
+  const executeDeleteRow = async () => {
+    const body = await fetchApi(
+      `/api/monitor/logininfor/${selectedRowKeys.join(",")}`,
+      push,
+      {
+        method: "DELETE",
+      }
+    );
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+
+        //删除按钮变回不可点击
+        setRowCanDelete(false);
+        //选中行数据重置为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //确定清空日志数据
+  const executeClear = async () => {
+    const body = await fetchApi("/api/monitor/logininfor/clean", push, {
+      method: "DELETE",
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("清空成功");
+        //选中行数据重置为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //确定解锁用户
+  const executeUnlock = async (userName: string) => {
+    const body = await fetchApi(
+      `/api/monitor/logininfor/unlock/${userName}`,
+      push
+    );
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("解锁成功");
+        //刷新列表
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionRef = useRef<ActionType>();
+  //表单对象引用
+  const formRef = useRef<ProFormInstance>();
+
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  //导出日志文件
+  const exportTable = async () => {
+    if (formRef.current) {
+      const formData = new FormData();
+
+      const data = {
+        pageNum: page,
+        pageSize: pageSize,
+        ...formRef.current.getFieldsValue(),
+      };
+
+      Object.keys(data).forEach((key) => {
+        if (data[key] !== undefined) {
+          formData.append(key, data[key]);
+        }
+      });
+
+      await fetchFile(
+        "/api/monitor/logininfor/export",
+        push,
+        {
+          method: "POST",
+          body: formData,
+        },
+        `logininfor_${new Date().getTime()}.xlsx`
+      );
+    }
+  };
+
+  return (
+    <PageContainer title={false}>
+      <ProTable
+        formRef={formRef}
+        rowKey="infoId"
+        rowSelection={{
+          selectedRowKeys,
+          ...rowSelection,
+        }}
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await getLog(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+          onChange: pageChange,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionRef}
+        toolbar={{
+          actions: [
+            <Button
+              key="danger"
+              danger
+              icon={<DeleteOutlined />}
+              disabled={!rowCanDelete}
+              onClick={onClickDeleteRow}
+            >
+              删除
+            </Button>,
+            <Button
+              key="clear"
+              danger
+              icon={<ClearOutlined />}
+              onClick={onClickClear}
+            >
+              清空
+            </Button>,
+            <Button
+              key="unlock"
+              icon={<UnlockOutlined />}
+              disabled={!rowCanUnlock}
+              onClick={onClickUnlock}
+            >
+              解锁
+            </Button>,
+            <Button
+              key="export"
+              type="primary"
+              icon={<FontAwesomeIcon icon={faDownload} />}
+              onClick={exportTable}
+            >
+              导出
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key?: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key?: string | undefined) => {
+                if (actionRef.current) {
+                  actionRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+    </PageContainer>
+  );
+}

+ 603 - 0
app/(business)/system/log/operlog/page.tsx

@@ -0,0 +1,603 @@
+"use client";
+
+import { fetchApi, fetchFile } from "@/app/_modules/func";
+import {
+  ClearOutlined,
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  EyeOutlined,
+  ReloadOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import {
+  PageContainer,
+  ProDescriptions,
+  ProTable,
+} from "@ant-design/pro-components";
+import { Button, message, Modal, Space, Tag } from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faCheck,
+  faDownload,
+  faToggleOff,
+  faToggleOn,
+  faXmark,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useRef, useState } from "react";
+
+export default function OperLog() {
+  const { push } = useRouter();
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "日志编号",
+      dataIndex: "operId",
+      search: false,
+    },
+    {
+      title: "系统模块",
+      fieldProps: {
+        placeholder: "请输入系统模块",
+      },
+      dataIndex: "title",
+      order: 9,
+    },
+    {
+      title: "操作类型",
+      fieldProps: {
+        placeholder: "请选择操作类型",
+      },
+      dataIndex: "businessType",
+      order: 7,
+      valueEnum: {
+        1: {
+          text: "新增",
+          status: "1",
+        },
+        2: {
+          text: "修改",
+          status: "2",
+        },
+        3: {
+          text: "删除",
+          status: "3",
+        },
+        4: {
+          text: "授权",
+          status: "4",
+        },
+        5: {
+          text: "导出",
+          status: "5",
+        },
+        6: {
+          text: "导入",
+          status: "6",
+        },
+        7: {
+          text: "强退",
+          status: "7",
+        },
+        8: {
+          text: "生成代码",
+          status: "8",
+        },
+        9: {
+          text: "清空数据",
+          status: "9",
+        },
+        0: {
+          text: "其他",
+          status: "0",
+        },
+      },
+    },
+    {
+      title: "操作人员",
+      fieldProps: {
+        placeholder: "请输入操作人员",
+      },
+      dataIndex: "operName",
+      sorter: true,
+      order: 8,
+    },
+    {
+      title: "操作地址",
+      fieldProps: {
+        placeholder: "请输入操作地址",
+      },
+      dataIndex: "operIp",
+      order: 10,
+    },
+    {
+      title: "操作地点",
+      dataIndex: "operLocation",
+      search: false,
+    },
+    {
+      title: "操作状态",
+      fieldProps: {
+        placeholder: "请选择操作状态",
+      },
+      dataIndex: "status",
+      valueType: "select",
+      render: (_, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.status == 0 ? "green" : "red"}
+              icon={
+                record.status == 0 ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {_}
+            </Tag>
+          </Space>
+        );
+      },
+      valueEnum: {
+        0: {
+          text: "成功",
+          status: "0",
+        },
+        1: {
+          text: "失败",
+          status: "1",
+        },
+      },
+      order: 6,
+    },
+    {
+      title: "操作时间",
+      dataIndex: "operTime",
+      valueType: "dateTime",
+      search: false,
+      sorter: true,
+    },
+    {
+      title: "操作时间",
+      fieldProps: {
+        placeholder: ["开始日期", "结束日期"],
+      },
+      dataIndex: "operTimeRange",
+      valueType: "dateRange",
+      hideInTable: true,
+      order: 5,
+      search: {
+        transform: (value) => {
+          return {
+            "params[beginTime]": `${value[0]} 00:00:00`,
+            "params[endTime]": `${value[1]} 23:59:59`,
+          };
+        },
+      },
+    },
+    {
+      title: "消耗时间",
+      dataIndex: "costTime",
+      sorter: true,
+      search: false,
+      render: (_, record) => {
+        return <span>{_}毫秒</span>;
+      },
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => [
+        <Button
+          key={record.operId}
+          type="link"
+          icon={<EyeOutlined />}
+          onClick={() => showRowModal(record)}
+        >
+          详情
+        </Button>,
+      ],
+    },
+  ];
+
+  //查询日志数据
+  const getLog = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(
+      `/api/monitor/operlog/list?${queryParams}`,
+      push
+    );
+    console.log("data:", body);
+    return body;
+  };
+
+  //删除按钮是否可用,选中行时才可用
+  const [rowCanDelete, setRowCanDelete] = useState(false);
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+    },
+  };
+
+  //点击删除按钮
+  const onClickDeleteRow = () => {
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定删除日志编号为“${selectedRowKeys.join(",")}”的数据项?`,
+      onOk() {
+        executeDeleteRow();
+      },
+      onCancel() {},
+    });
+  };
+
+  //点击清空按钮
+  const onClickClear = () => {
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: "是否确认清空所有操作日志数据项?",
+      onOk() {
+        executeClear();
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的日志数据
+  const executeDeleteRow = async () => {
+    const body = await fetchApi(
+      `/api/monitor/operlog/${selectedRowKeys.join(",")}`,
+      push,
+      {
+        method: "DELETE",
+      }
+    );
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+
+        //删除按钮变回不可点击
+        setRowCanDelete(false);
+        //选中的数据恢复为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //确定清空日志数据
+  const executeClear = async () => {
+    const body = await fetchApi("/api/monitor/operlog/clean", push, {
+      method: "DELETE",
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("清空成功");
+        //选中的数据恢复为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //控制是否展示行详情模态框
+  const [isModalOpen, setIsModalOpen] = useState(false);
+
+  //关闭行详情展示
+  function closeRowModal() {
+    setIsModalOpen(false);
+  }
+
+  const [selectedRow, setSelectedRow] = useState(undefined as any);
+
+  //展示行详情
+  function showRowModal(record: any) {
+    setIsModalOpen(true);
+    setSelectedRow(record);
+  }
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionRef = useRef<ActionType>();
+  //表单对象引用
+  const formRef = useRef<ProFormInstance>();
+
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  //导出日志文件
+  const exportTable = async () => {
+    if (formRef.current) {
+      const formData = new FormData();
+
+      const data = {
+        pageNum: page,
+        pageSize: pageSize,
+        ...formRef.current.getFieldsValue(),
+      };
+
+      Object.keys(data).forEach((key) => {
+        if (data[key] !== undefined) {
+          formData.append(key, data[key]);
+        }
+      });
+
+      await fetchFile(
+        "/api/monitor/operlog/export",
+        push,
+        {
+          method: "POST",
+          body: formData,
+        },
+        `operlog_${new Date().getTime()}.xlsx`
+      );
+    }
+  };
+
+  return (
+    <PageContainer title={false}>
+      <ProTable
+        formRef={formRef}
+        rowKey="operId"
+        rowSelection={{
+          selectedRowKeys,
+          ...rowSelection,
+        }}
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          console.log(params, sorter, filter);
+          const data = await getLog(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+          onChange: pageChange,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionRef}
+        toolbar={{
+          actions: [
+            <Button
+              key="danger"
+              danger
+              icon={<DeleteOutlined />}
+              disabled={!rowCanDelete}
+              onClick={onClickDeleteRow}
+            >
+              删除
+            </Button>,
+            <Button
+              key="clear"
+              danger
+              icon={<ClearOutlined />}
+              onClick={onClickClear}
+            >
+              清空
+            </Button>,
+            <Button
+              key="export"
+              type="primary"
+              icon={<FontAwesomeIcon icon={faDownload} />}
+              onClick={exportTable}
+            >
+              导出
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionRef.current) {
+                  actionRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+
+      {selectedRow !== undefined && (
+        <Modal
+          title="操作日志详情"
+          footer={<Button onClick={closeRowModal}>关闭</Button>}
+          open={isModalOpen}
+          onCancel={closeRowModal}
+        >
+          <ProDescriptions column={10}>
+            <ProDescriptions.Item span={3} label="操作模块">
+              {selectedRow.title} /
+            </ProDescriptions.Item>
+            <ProDescriptions.Item
+              span={2}
+              valueEnum={{
+                1: {
+                  text: "新增",
+                  status: "1",
+                },
+                2: {
+                  text: "修改",
+                  status: "2",
+                },
+                3: {
+                  text: "删除",
+                  status: "3",
+                },
+                4: {
+                  text: "授权",
+                  status: "4",
+                },
+                5: {
+                  text: "导出",
+                  status: "5",
+                },
+                6: {
+                  text: "导入",
+                  status: "6",
+                },
+                7: {
+                  text: "强退",
+                  status: "7",
+                },
+                8: {
+                  text: "生成代码",
+                  status: "8",
+                },
+                9: {
+                  text: "清空数据",
+                  status: "9",
+                },
+                0: {
+                  text: "其他",
+                  status: "0",
+                },
+              }}
+            >
+              {selectedRow.businessType}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item span={5} label="请求地址">
+              {selectedRow.operUrl}
+            </ProDescriptions.Item>
+          </ProDescriptions>
+          <ProDescriptions column={6}>
+            <ProDescriptions.Item span={3} label="登录信息">
+              {selectedRow.operName}/{selectedRow.operIp}/
+              {selectedRow.operLocation}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item span={3} label="请求方式">
+              {selectedRow.requestMethod}
+            </ProDescriptions.Item>
+
+            <ProDescriptions.Item span={6} label="操作方法">
+              {selectedRow.method}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item span={6} label="请求参数">
+              {selectedRow.operParam}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item span={6} label="返回参数">
+              {selectedRow.jsonResult}
+            </ProDescriptions.Item>
+          </ProDescriptions>
+
+          <ProDescriptions column={8}>
+            <ProDescriptions.Item
+              span={2}
+              label="操作状态"
+              valueEnum={{
+                0: {
+                  text: "成功",
+                  status: "0",
+                },
+                1: {
+                  text: "失败",
+                  status: "1",
+                },
+              }}
+            >
+              {selectedRow.status}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item span={2} label="消耗时间">
+              {selectedRow.costTime}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item span={4} label="操作时间">
+              {selectedRow.operTime}
+            </ProDescriptions.Item>
+          </ProDescriptions>
+          {selectedRow.errorMsg && (
+            <ProDescriptions>
+              <ProDescriptions.Item label="异常信息">
+                {selectedRow.errorMsg}
+              </ProDescriptions.Item>
+            </ProDescriptions>
+          )}
+        </Modal>
+      )}
+    </PageContainer>
+  );
+}

+ 887 - 0
app/(business)/system/menu/page.tsx

@@ -0,0 +1,887 @@
+"use client";
+
+import { fetchApi } from "@/app/_modules/func";
+import {
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  PlusOutlined,
+  ReloadOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import {
+  ModalForm,
+  PageContainer,
+  ProForm,
+  ProFormDigit,
+  ProFormRadio,
+  ProFormSelect,
+  ProFormText,
+  ProFormTreeSelect,
+  ProTable,
+} from "@ant-design/pro-components";
+import { Button, message, Modal, Space, Tag } from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faArrowsUpDown,
+  faCheck,
+  faPenToSquare,
+  faToggleOff,
+  faToggleOn,
+  faXmark,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { IconMap } from "@/app/_modules/definies";
+import { useRef, useState } from "react";
+
+//查询表格数据API
+const queryAPI = "/api/system/menu/list";
+//新建数据API
+const newAPI = "/api/system/menu";
+//修改数据API
+const modifyAPI = "/api/system/menu";
+//查询详情数据API
+const queryDetailAPI = "/api/system/menu";
+//删除API
+const deleteAPI = "/api/system/menu";
+
+export default function Menu() {
+  const { push } = useRouter();
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "菜单名称",
+      fieldProps: {
+        placeholder: "请输入菜单名称",
+      },
+      dataIndex: "menuName",
+      order: 2,
+    },
+    {
+      title: "图标",
+      dataIndex: "icon",
+      search: false,
+    },
+    {
+      title: "排序",
+      dataIndex: "orderNum",
+      search: false,
+    },
+    {
+      title: "权限标识",
+      dataIndex: "perms",
+      ellipsis: true,
+      search: false,
+    },
+    {
+      title: "状态",
+      fieldProps: {
+        placeholder: "请选择菜单状态",
+      },
+      dataIndex: "status",
+      valueType: "select",
+      render: (_, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.status === "0" ? "green" : "red"}
+              icon={
+                record.status == 0 ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {_}
+            </Tag>
+          </Space>
+        );
+      },
+      valueEnum: {
+        0: {
+          text: "正常",
+          status: "0",
+        },
+        1: {
+          text: "停用",
+          status: "1",
+        },
+      },
+      order: 1,
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      search: false,
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => [
+        <Button
+          key="modifyBtn"
+          type="link"
+          icon={<FontAwesomeIcon icon={faPenToSquare} />}
+          onClick={() => onClickShowRowModifyModal(record)}
+        >
+          修改
+        </Button>,
+        <Button
+          key="newBtn"
+          type="link"
+          icon={<PlusOutlined />}
+          onClick={() => onClickAdd(record)}
+        >
+          新建
+        </Button>,
+        <Button
+          key="deleteBtn"
+          type="link"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => onClickDeleteRow(record)}
+        >
+          删除
+        </Button>,
+      ],
+    },
+  ];
+
+  //0.查询表格数据
+
+  //原始的可展开的所有行的 id
+  const [defaultExpandKeys, setDefaultExpandKeys] = useState<any[]>([]);
+
+  //控制行展开的数据
+  const [expandKeys, setExpandKeys] = useState<any[]>([]);
+
+  const queryTableData = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      ...params,
+    };
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(`${queryAPI}?${queryParams}`, push);
+
+    const firstLevel = getFirstLevel(body.data);
+
+    firstLevel.forEach((first) => {
+      getChildren(body.data, first);
+    });
+
+    const newExpandedKeys: any[] = [];
+    const render = (treeDatas: any[]) => {
+      // 获取到所有可展开的父节点
+      treeDatas.map((item) => {
+        if (item.children) {
+          newExpandedKeys.push(item.menuId);
+          render(item.children);
+        }
+      });
+      return newExpandedKeys;
+    };
+
+    const keys = render(firstLevel);
+    setDefaultExpandKeys(keys);
+    setExpandKeys([]);
+    return firstLevel;
+  };
+
+  const getFirstLevel = (data: any[]) => {
+    const firstLevel: any[] = [];
+    for (let index = 0; index < data.length; index++) {
+      const item = data[index];
+      if (item.parentId === 0) {
+        firstLevel.push(item);
+      }
+    }
+    return firstLevel;
+  };
+
+  const getChildren = (data: any[], parentNode: any) => {
+    for (let index = 0; index < data.length; index++) {
+      const item = data[index];
+      if (item.parentId === parentNode.menuId) {
+        parentNode.children.push(item);
+        getChildren(data, item);
+      }
+    }
+
+    if (parentNode.children.length == 0) {
+      delete parentNode.children;
+    }
+  };
+
+  //1.新建
+
+  const [showAddModal, setShowAddModal] = useState(false);
+
+  //新建表单是否带有父节点id
+  const [rowParentId, setRowParentId] = useState(0);
+
+  //点击新建,如果从行点击新建,给定父节点
+  const onClickAdd = (record?: any) => {
+    setRowParentId(record.menuId);
+    setShowAddModal(true);
+  };
+
+  const cancelAddModal = () => {
+    setShowAddModal(false);
+    setRowParentId(0);
+  };
+
+  //确定新建数据
+  const executeAddData = async (values: any) => {
+    const body = await fetchApi(newAPI, push, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        setShowAddModal(false);
+        return true;
+      }
+
+      message.error(body.msg);
+      return false;
+    }
+    return false;
+  };
+
+  //2.修改
+
+  //是否展示修改对话框
+  const [isShowModifyDataModal, setIsShowModifyDataModal] = useState(false);
+
+  //展示修改对话框
+  const onClickShowRowModifyModal = (record: any) => {
+    queryRowData(record);
+    setIsShowModifyDataModal(true);
+  };
+
+  //修改数据表单引用
+  const modifyFormRef = useRef<ProFormInstance>();
+
+  //操作当前数据的附加数据
+  const [operatRowData, setOperateRowData] = useState<{
+    [key: string]: any;
+  }>({});
+
+  //查询并加载待修改数据的详细信息
+  const queryRowData = async (record: any) => {
+    const menuId = record.menuId;
+
+    operatRowData["menuId"] = menuId;
+
+    setOperateRowData(operatRowData);
+
+    if (menuId !== undefined) {
+      const body = await fetchApi(`${queryDetailAPI}/${menuId}`, push);
+
+      if (body !== undefined) {
+        if (body.code == 200) {
+          modifyFormRef?.current?.setFieldsValue({
+            //需要加载到修改表单中的数据
+            parentId: body.data.parentId,
+            menuName: body.data.menuName,
+            orderNum: body.data.orderNum,
+            path: body.data.path,
+            isFrame: body.data.isFrame,
+            menuType: body.data.menuType,
+            perms: body.data.perms,
+            icon: body.data.icon,
+            visible: body.data.visible,
+            status: body.data.status,
+          });
+
+          setIsCatalog(body.data.menuType === "M");
+          setIsMenu(body.data.menuType === "C");
+          setIsButton(body.data.menuType === "F");
+        }
+      }
+    }
+  };
+
+  //确认修改数据
+  const executeModifyData = async (values: any) => {
+    values["menuId"] = operatRowData["menuId"];
+
+    const body = await fetchApi(modifyAPI, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        setIsShowModifyDataModal(false);
+        return true;
+      }
+      message.error(body.msg);
+      return false;
+    }
+  };
+
+  //3.展开/折叠
+
+  //点击展开/折叠按钮
+  const onClickExpandRow = () => {
+    if (expandKeys.length > 0) {
+      setExpandKeys([]);
+    } else {
+      setExpandKeys(defaultExpandKeys);
+    }
+  };
+
+  //处理行的展开/折叠逻辑
+  const handleExpand = (expanded: boolean, record: any) => {
+    console.log("has keys:", expandKeys);
+    let keys = [...expandKeys];
+
+    if (expanded) {
+      keys.push(record.menuId);
+    } else {
+      keys = keys.filter((key: number) => key !== record.menuId);
+    }
+    console.log("now keys:", keys);
+    setExpandKeys(keys);
+  };
+
+  //4.导出
+
+  //5.选择行
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionTableRef = useRef<ActionType>();
+  //搜索表单对象引用
+  const searchTableFormRef = useRef<ProFormInstance>();
+
+  const getMenuList = async () => {
+    const body = await fetchApi(queryAPI, push);
+    if (body !== undefined) {
+      const firstLevel = getFirstLevel(body.data);
+
+      firstLevel.forEach((first) => {
+        getChildren(body.data, first);
+      });
+
+      const root: any = {
+        menuId: 0,
+        menuName: "根目录",
+        children: [],
+      };
+
+      firstLevel.forEach((first: any) => {
+        root.children.push(first as never);
+      });
+
+      return [root];
+    }
+
+    return [];
+  };
+
+  //点击删除按钮
+  const onClickDeleteRow = (record: any) => {
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定删除菜单名称为“${record.menuName}”的数据项?`,
+      onOk() {
+        executeDeleteRow(record.menuId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的菜单
+  const executeDeleteRow = async (menuId: any) => {
+    const body = await fetchApi(`${deleteAPI}/${menuId}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  const [isCatalog, setIsCatalog] = useState(true);
+  const [isMenu, setIsMenu] = useState(false);
+  const [isButton, setIsButton] = useState(false);
+
+  const onChangeType = (e: any) => {
+    const type = e.target.value;
+    setIsCatalog(type === "M");
+    setIsMenu(type === "C");
+    setIsButton(type === "F");
+  };
+
+  const IconData = () => {
+    const iconData = { ...IconMap };
+    Object.keys(iconData).forEach((key) => {
+      iconData[key] = (
+        <>
+          <span style={{ marginRight: 8 }}>{iconData[key]}</span>
+          {key}
+        </>
+      );
+    });
+    return iconData;
+  };
+
+  return (
+    <PageContainer title={false}>
+      <ProTable
+        formRef={searchTableFormRef}
+        rowKey="menuId"
+        columns={columns}
+        expandable={{
+          expandedRowKeys: expandKeys,
+          onExpand: handleExpand,
+        }}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await queryTableData(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data,
+              success: true,
+              total: data.length,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={false}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionTableRef}
+        toolbar={{
+          actions: [
+            <ModalForm
+              key="addmodal"
+              title="添加菜单"
+              open={showAddModal}
+              trigger={
+                <Button icon={<PlusOutlined />} type="primary">
+                  新建
+                </Button>
+              }
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+                onCancel: () => {
+                  cancelAddModal();
+                },
+              }}
+              submitTimeout={2000}
+              onFinish={executeAddData}
+            >
+              <ProForm.Group>
+                <ProFormTreeSelect
+                  width="md"
+                  name="parentId"
+                  initialValue={rowParentId}
+                  label="上级菜单"
+                  placeholder="请选择上级菜单"
+                  rules={[{ required: true, message: "请选择上级菜单" }]}
+                  request={getMenuList}
+                  fieldProps={{
+                    filterTreeNode: true,
+                    showSearch: true,
+                    treeNodeFilterProp: "label",
+                    fieldNames: {
+                      label: "menuName",
+                      value: "menuId",
+                    },
+                  }}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormRadio.Group
+                  name="menuType"
+                  width="md"
+                  label="类型"
+                  initialValue="M"
+                  fieldProps={{
+                    onChange: (e: any) => onChangeType(e),
+                  }}
+                  options={[
+                    {
+                      label: "目录",
+                      value: "M",
+                    },
+                    {
+                      label: "菜单",
+                      value: "C",
+                    },
+                    {
+                      label: "按钮",
+                      value: "F",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              {(isCatalog || isMenu) && (
+                <ProForm.Group>
+                  <ProFormSelect
+                    width="md"
+                    name="icon"
+                    label="菜单图标"
+                    fieldProps={{
+                      showSearch,
+                    }}
+                    valueEnum={IconData}
+                    placeholder="请选择菜单图标"
+                    rules={[{ required: true, message: "请选择菜单图标" }]}
+                  />
+                </ProForm.Group>
+              )}
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="menuName"
+                  label="菜单名称"
+                  placeholder="请输入菜单名称"
+                  rules={[{ required: true, message: "请输入菜单名称" }]}
+                />
+                <ProFormDigit
+                  fieldProps={{ precision: 0 }}
+                  width="md"
+                  name="orderNum"
+                  initialValue="1"
+                  label="排序"
+                  placeholder="请输入排序"
+                  rules={[{ required: true, message: "请输入排序" }]}
+                />
+              </ProForm.Group>
+              {(isCatalog || isMenu) && (
+                <ProForm.Group>
+                  <ProFormText
+                    width="md"
+                    name="path"
+                    label="路由地址"
+                    placeholder="请输入路由地址"
+                    rules={[{ required: true, message: "请输入路由地址" }]}
+                  />
+                  <ProFormRadio.Group
+                    name="isFrame"
+                    width="md"
+                    label="是否外链"
+                    initialValue="1"
+                    options={[
+                      {
+                        label: "是",
+                        value: "0",
+                      },
+                      {
+                        label: "否",
+                        value: "1",
+                      },
+                    ]}
+                  />
+                </ProForm.Group>
+              )}
+
+              {isMenu && (
+                <ProForm.Group>
+                  <ProFormText
+                    width="md"
+                    name="perms"
+                    label="权限字符"
+                    placeholder="请输入权限字符"
+                  />
+                </ProForm.Group>
+              )}
+
+              <ProForm.Group>
+                <ProFormRadio.Group
+                  name="visible"
+                  width="md"
+                  label="显示状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "显示",
+                      value: "0",
+                    },
+                    {
+                      label: "隐藏",
+                      value: "1",
+                    },
+                  ]}
+                />
+                <ProFormRadio.Group
+                  name="status"
+                  width="md"
+                  label="菜单状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "停用",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+            </ModalForm>,
+            <Button
+              key="expand"
+              icon={<FontAwesomeIcon icon={faArrowsUpDown} />}
+              onClick={() => onClickExpandRow()}
+            >
+              折叠/展开
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionTableRef.current) {
+                  actionTableRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+      <ModalForm
+        key="modifymodal"
+        title="修改菜单"
+        formRef={modifyFormRef}
+        open={isShowModifyDataModal}
+        autoFocusFirstInput
+        modalProps={{
+          destroyOnHidden: true,
+          onCancel: () => {
+            setIsShowModifyDataModal(false);
+          },
+        }}
+        submitTimeout={2000}
+        onFinish={executeModifyData}
+      >
+        <ProForm.Group>
+          <ProFormTreeSelect
+            width="md"
+            name="parentId"
+            initialValue={rowParentId}
+            label="上级菜单"
+            placeholder="请选择上级菜单"
+            rules={[{ required: true, message: "请选择上级菜单" }]}
+            request={getMenuList}
+            fieldProps={{
+              filterTreeNode: true,
+              showSearch: true,
+              treeNodeFilterProp: "label",
+              fieldNames: {
+                label: "menuName",
+                value: "menuId",
+              },
+            }}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormRadio.Group
+            name="menuType"
+            width="md"
+            label="类型"
+            fieldProps={{
+              onChange: (e: any) => onChangeType(e),
+            }}
+            options={[
+              {
+                label: "目录",
+                value: "M",
+              },
+              {
+                label: "菜单",
+                value: "C",
+              },
+              {
+                label: "按钮",
+                value: "F",
+              },
+            ]}
+          />
+        </ProForm.Group>
+        {(isCatalog || isMenu) && (
+          <ProForm.Group>
+            <ProFormSelect
+              width="md"
+              name="icon"
+              label="菜单图标"
+              fieldProps={{
+                showSearch,
+              }}
+              valueEnum={IconData}
+              placeholder="请选择菜单图标"
+              rules={[{ required: true, message: "请选择菜单图标" }]}
+            />
+          </ProForm.Group>
+        )}
+        <ProForm.Group>
+          <ProFormText
+            width="md"
+            name="menuName"
+            label="菜单名称"
+            placeholder="请输入菜单名称"
+            rules={[{ required: true, message: "请输入菜单名称" }]}
+          />
+          <ProFormDigit
+            fieldProps={{ precision: 0 }}
+            width="md"
+            name="orderNum"
+            initialValue="1"
+            label="排序"
+            placeholder="请输入排序"
+            rules={[{ required: true, message: "请输入排序" }]}
+          />
+        </ProForm.Group>
+        {(isCatalog || isMenu) && (
+          <ProForm.Group>
+            <ProFormText
+              width="md"
+              name="path"
+              label="路由地址"
+              placeholder="请输入路由地址"
+              rules={[{ required: true, message: "请输入路由地址" }]}
+            />
+            <ProFormRadio.Group
+              name="isFrame"
+              width="md"
+              label="是否外链"
+              initialValue="1"
+              options={[
+                {
+                  label: "是",
+                  value: "0",
+                },
+                {
+                  label: "否",
+                  value: "1",
+                },
+              ]}
+            />
+          </ProForm.Group>
+        )}
+
+        {isMenu && (
+          <ProForm.Group>
+            <ProFormText
+              width="md"
+              name="perms"
+              label="权限字符"
+              placeholder="请输入权限字符"
+            />
+          </ProForm.Group>
+        )}
+
+        <ProForm.Group>
+          <ProFormRadio.Group
+            name="visible"
+            width="md"
+            label="显示状态"
+            initialValue="0"
+            options={[
+              {
+                label: "显示",
+                value: "0",
+              },
+              {
+                label: "隐藏",
+                value: "1",
+              },
+            ]}
+          />
+          <ProFormRadio.Group
+            name="status"
+            width="md"
+            label="菜单状态"
+            initialValue="0"
+            options={[
+              {
+                label: "正常",
+                value: "0",
+              },
+              {
+                label: "停用",
+                value: "1",
+              },
+            ]}
+          />
+        </ProForm.Group>
+      </ModalForm>
+      ,
+    </PageContainer>
+  );
+}

+ 616 - 0
app/(business)/system/notice/page.tsx

@@ -0,0 +1,616 @@
+"use client";
+
+import { fetchApi } from "@/app/_modules/func";
+import {
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  PlusOutlined,
+  ReloadOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import {
+  ModalForm,
+  PageContainer,
+  ProForm,
+  ProFormRadio,
+  ProFormSelect,
+  ProFormText,
+  ProTable,
+} from "@ant-design/pro-components";
+import { Button, message, Modal, Space, Tag } from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faCheck,
+  faPenToSquare,
+  faToggleOff,
+  faToggleOn,
+  faXmark,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useRef, useState } from "react";
+
+import dynamic from "next/dynamic";
+import "react-quill/dist/quill.snow.css";
+const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
+
+//查询表格数据API
+const queryAPI = "/api/system/notice/list";
+//新建数据API
+const newAPI = "/api/system/notice";
+//修改数据API
+const modifyAPI = "/api/system/notice";
+//查询详情数据API
+const queryDetailAPI = "/api/system/notice";
+//删除API
+const deleteAPI = "/api/system/notice";
+
+export default function Notice() {
+  const { push } = useRouter();
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "公告编号",
+      dataIndex: "noticeId",
+      search: false,
+    },
+    {
+      title: "公告标题",
+      fieldProps: {
+        placeholder: "请输入公告标题",
+      },
+      dataIndex: "noticeTitle",
+      ellipsis: true,
+      sorter: true,
+      order: 3,
+    },
+    {
+      title: "公告类型",
+      fieldProps: {
+        placeholder: "请选择公告类型",
+      },
+      dataIndex: "noticeType",
+      valueType: "select",
+      render: (_, record) => {
+        return (
+          <Space>
+            <Tag color={record.noticeType === "2" ? "green" : "orange"}>
+              {_}
+            </Tag>
+          </Space>
+        );
+      },
+      valueEnum: {
+        2: {
+          text: "公告",
+          status: "2",
+        },
+        1: {
+          text: "通知",
+          status: "1",
+        },
+      },
+      order: 1,
+    },
+    {
+      title: "状态",
+      dataIndex: "status",
+      valueType: "select",
+      search: false,
+      render: (_, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.status === "0" ? "green" : "red"}
+              icon={
+                record.status === "0" ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {_}
+            </Tag>
+          </Space>
+        );
+      },
+      valueEnum: {
+        0: {
+          text: "正常",
+          status: "0",
+        },
+        1: {
+          text: "关闭",
+          status: "1",
+        },
+      },
+    },
+    {
+      title: "创建者",
+      fieldProps: {
+        placeholder: "请输入创建者",
+      },
+      dataIndex: "createBy",
+      order: 2,
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      search: false,
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => [
+        <Button
+          key="modifyBtn"
+          type="link"
+          icon={<FontAwesomeIcon icon={faPenToSquare} />}
+          onClick={() => onClickShowRowModifyModal(record)}
+        >
+          修改
+        </Button>,
+        <Button
+          key="deleteBtn"
+          type="link"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => onClickDeleteRow(record)}
+        >
+          删除
+        </Button>,
+      ],
+    },
+  ];
+
+  //0.查询表格数据
+  const queryTableData = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(`${queryAPI}?${queryParams}`, push);
+
+    return body;
+  };
+
+  //1.新建
+
+  //确定新建数据
+  const executeAddData = async (values: any) => {
+    const body = await fetchApi(newAPI, push, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        return true;
+      }
+
+      message.error(body.msg);
+      return false;
+    }
+    return false;
+  };
+
+  //2.修改
+
+  //是否展示修改对话框
+  const [isShowModifyDataModal, setIsShowModifyDataModal] = useState(false);
+
+  //展示修改对话框
+  const onClickShowRowModifyModal = (record?: any) => {
+    queryRowData(record);
+    setIsShowModifyDataModal(true);
+  };
+
+  //修改数据表单引用
+  const modifyFormRef = useRef<ProFormInstance>();
+
+  //操作当前数据的附加数据
+  const [operatRowData, setOperateRowData] = useState<{
+    [key: string]: any;
+  }>({});
+
+  //查询并加载待修改数据的详细信息
+  const queryRowData = async (record?: any) => {
+    const noticeId =
+      record !== undefined ? record.noticeId : selectedRow.noticeId;
+
+    operatRowData["noticeId"] = noticeId;
+
+    setOperateRowData(operatRowData);
+
+    if (noticeId !== undefined) {
+      const body = await fetchApi(`${queryDetailAPI}/${noticeId}`, push);
+
+      if (body !== undefined) {
+        if (body.code == 200) {
+          modifyFormRef?.current?.setFieldsValue({
+            //需要加载到修改表单中的数据
+            noticeTitle: body.data.noticeTitle,
+            noticeType: body.data.noticeType,
+            status: body.data.status,
+            noticeContent: body.data.noticeContent,
+          });
+        }
+      }
+    }
+  };
+
+  //确认修改数据
+  const executeModifyData = async (values: any) => {
+    values["noticeId"] = operatRowData["noticeId"];
+
+    const body = await fetchApi(modifyAPI, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        setIsShowModifyDataModal(false);
+        return true;
+      }
+      message.error(body.msg);
+      return false;
+    }
+  };
+
+  //3.删除
+
+  //点击删除按钮,展示删除确认框
+  const onClickDeleteRow = (record?: any) => {
+    const noticeId =
+      record != undefined ? record.noticeId : selectedRowKeys.join(",");
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `是否确认删除公告编号为“${noticeId}”的数据项?`,
+      onOk() {
+        executeDeleteRow(noticeId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的数据
+  const executeDeleteRow = async (dictId: any) => {
+    const body = await fetchApi(`${deleteAPI}/${dictId}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+
+        //修改按钮变回不可点击
+        setRowCanModify(false);
+        //删除按钮变回不可点击
+        setRowCanDelete(false);
+        //选中行数据重置为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //4.导出
+
+  //5.选择行
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [selectedRow, setSelectedRow] = useState(undefined as any);
+
+  //修改按钮是否可用,选中行时才可用
+  const [rowCanModify, setRowCanModify] = useState(false);
+
+  //删除按钮是否可用,选中行时才可用
+  const [rowCanDelete, setRowCanDelete] = useState(false);
+
+  //ProTable rowSelection
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[], selectedRows: any[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+
+      if (newSelectedRowKeys && newSelectedRowKeys.length == 1) {
+        setSelectedRow(selectedRows[0]);
+        setRowCanModify(true);
+      } else {
+        setRowCanModify(false);
+        setSelectedRow(undefined);
+      }
+    },
+
+    //复选框的额外禁用判断
+    // getCheckboxProps: (record) => ({
+    //   disabled: record.userId == 1,
+    // }),
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionTableRef = useRef<ActionType>();
+  //搜索表单对象引用
+  const searchTableFormRef = useRef<ProFormInstance>();
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  return (
+    <PageContainer title={false}>
+      <ProTable
+        formRef={searchTableFormRef}
+        rowKey="noticeId"
+        rowSelection={{
+          selectedRowKeys,
+          ...rowSelection,
+        }}
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await queryTableData(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+          onChange: pageChange,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionTableRef}
+        toolbar={{
+          actions: [
+            <ModalForm
+              key="addmodal"
+              title="添加公告"
+              trigger={
+                <Button icon={<PlusOutlined />} type="primary">
+                  新建
+                </Button>
+              }
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+              }}
+              submitTimeout={2000}
+              onFinish={executeAddData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="noticeTitle"
+                  label="公告标题"
+                  placeholder="请输入公告标题"
+                  rules={[{ required: true, message: "请输入公告标题" }]}
+                />
+                <ProFormSelect
+                  width="md"
+                  name="noticeType"
+                  label="公告类型"
+                  valueEnum={{
+                    "2": "公告",
+                    "1": "通知",
+                  }}
+                  placeholder="请选择公告类型"
+                  rules={[{ required: true, message: "请选择公告类型" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormRadio.Group
+                  name="status"
+                  width="sm"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "关闭",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProForm.Item
+                  name="noticeContent"
+                  style={{ width: 688, height: 240 }}
+                  label="内容"
+                >
+                  <ReactQuill
+                    theme="snow"
+                    placeholder="请输入内容"
+                    style={{ height: 160 }}
+                  />
+                </ProForm.Item>
+              </ProForm.Group>
+            </ModalForm>,
+            <ModalForm
+              key="modifymodal"
+              title="修改公告"
+              formRef={modifyFormRef}
+              trigger={
+                <Button
+                  icon={<FontAwesomeIcon icon={faPenToSquare} />}
+                  disabled={!rowCanModify}
+                  onClick={() => onClickShowRowModifyModal()}
+                >
+                  修改
+                </Button>
+              }
+              open={isShowModifyDataModal}
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+                onCancel: () => {
+                  setIsShowModifyDataModal(false);
+                },
+              }}
+              submitTimeout={2000}
+              onFinish={executeModifyData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="noticeTitle"
+                  label="公告标题"
+                  placeholder="请输入公告标题"
+                  rules={[{ required: true, message: "请输入公告标题" }]}
+                />
+                <ProFormSelect
+                  width="md"
+                  name="noticeType"
+                  label="公告类型"
+                  valueEnum={{
+                    "2": "公告",
+                    "1": "通知",
+                  }}
+                  placeholder="请选择公告类型"
+                  rules={[{ required: true, message: "请选择公告类型" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormRadio.Group
+                  name="status"
+                  width="sm"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "关闭",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProForm.Item
+                  name="noticeContent"
+                  style={{ width: 688, height: 240 }}
+                  label="内容"
+                >
+                  <ReactQuill
+                    theme="snow"
+                    placeholder="请输入内容"
+                    style={{ height: 160 }}
+                  />
+                </ProForm.Item>
+              </ProForm.Group>
+            </ModalForm>,
+
+            <Button
+              key="danger"
+              danger
+              icon={<DeleteOutlined />}
+              disabled={!rowCanDelete}
+              onClick={() => onClickDeleteRow()}
+            >
+              删除
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionTableRef.current) {
+                  actionTableRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+    </PageContainer>
+  );
+}

+ 634 - 0
app/(business)/system/post/page.tsx

@@ -0,0 +1,634 @@
+"use client";
+
+import { fetchApi, fetchFile } from "@/app/_modules/func";
+import {
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  PlusOutlined,
+  ReloadOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import {
+  ModalForm,
+  PageContainer,
+  ProForm,
+  ProFormDigit,
+  ProFormRadio,
+  ProFormText,
+  ProFormTextArea,
+  ProTable,
+} from "@ant-design/pro-components";
+import { Button, message, Modal, Space, Tag } from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faCheck,
+  faDownload,
+  faPenToSquare,
+  faToggleOff,
+  faToggleOn,
+  faXmark,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import React, { useRef, useState } from "react";
+
+//查询表格数据API
+const queryAPI = "/api/system/post/list";
+//新建数据API
+const newAPI = "/api/system/post";
+//修改数据API
+const modifyAPI = "/api/system/post";
+//查询详情数据API
+const queryDetailAPI = "/api/system/post";
+//删除API
+const deleteAPI = "/api/system/post";
+//导出API
+const exportAPI = "/api/system/post/export";
+//导出文件前缀名
+const exportFilePrefix = "post";
+
+export default function Post() {
+  const { push } = useRouter();
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "岗位编号",
+      dataIndex: "postId",
+      search: false,
+    },
+    {
+      title: "岗位编码",
+      fieldProps: {
+        placeholder: "请输入岗位编码",
+      },
+      dataIndex: "postCode",
+      ellipsis: true,
+      order: 3,
+    },
+    {
+      title: "岗位名称",
+      fieldProps: {
+        placeholder: "请输入岗位名称",
+      },
+      dataIndex: "postName",
+      ellipsis: true,
+      sorter: true,
+      order: 2,
+    },
+    {
+      title: "岗位排序",
+      dataIndex: "postSort",
+      search: false,
+      sorter: true,
+    },
+    {
+      title: "状态",
+      fieldProps: {
+        placeholder: "请选择岗位状态",
+      },
+      dataIndex: "status",
+      valueType: "select",
+      render: (_, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.status === "0" ? "green" : "red"}
+              icon={
+                record.status == 0 ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {_}
+            </Tag>
+          </Space>
+        );
+      },
+      valueEnum: {
+        0: {
+          text: "正常",
+          status: "0",
+        },
+        1: {
+          text: "停用",
+          status: "1",
+        },
+      },
+      order: 1,
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      sorter: true,
+      search: false,
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => [
+        <Button
+          key="modifyBtn"
+          type="link"
+          icon={<FontAwesomeIcon icon={faPenToSquare} />}
+          onClick={() => onClickShowRowModifyModal(record)}
+        >
+          修改
+        </Button>,
+        <Button
+          key="deleteBtn"
+          type="link"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => onClickDeleteRow(record)}
+        >
+          删除
+        </Button>,
+      ],
+    },
+  ];
+
+  //0.查询表格数据
+  const queryTableData = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(`${queryAPI}?${queryParams}`, push);
+
+    return body;
+  };
+
+  //1.新建
+
+  //确定新建数据
+  const executeAddData = async (values: any) => {
+    const body = await fetchApi(newAPI, push, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        return true;
+      }
+
+      message.error(body.msg);
+      return false;
+    }
+    return false;
+  };
+
+  //2.修改
+
+  //是否展示修改对话框
+  const [isShowModifyDataModal, setIsShowModifyDataModal] = useState(false);
+
+  //展示修改对话框
+  const onClickShowRowModifyModal = (record?: any) => {
+    queryRowData(record);
+    setIsShowModifyDataModal(true);
+  };
+
+  //修改数据表单引用
+  const modifyFormRef = useRef<ProFormInstance>();
+
+  //操作当前数据的附加数据
+  const [operatRowData, setOperateRowData] = useState<{
+    [key: string]: any;
+  }>({});
+
+  //查询并加载待修改数据的详细信息
+  const queryRowData = async (record?: any) => {
+    const postId = record !== undefined ? record.postId : selectedRow.postId;
+
+    operatRowData["postId"] = postId;
+
+    setOperateRowData(operatRowData);
+
+    if (postId !== undefined) {
+      const body = await fetchApi(`${queryDetailAPI}/${postId}`, push);
+
+      if (body !== undefined) {
+        if (body.code == 200) {
+          modifyFormRef?.current?.setFieldsValue({
+            //需要加载到修改表单中的数据
+            postName: body.data.postName,
+            postCode: body.data.postCode,
+            postSort: body.data.postSort,
+            status: body.data.status,
+            remark: body.data.remark,
+          });
+        }
+      }
+    }
+  };
+
+  //确认修改数据
+  const executeModifyData = async (values: any) => {
+    values["postId"] = operatRowData["postId"];
+
+    const body = await fetchApi(modifyAPI, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+        setIsShowModifyDataModal(false);
+        return true;
+      }
+      message.error(body.msg);
+      return false;
+    }
+  };
+
+  //3.删除
+
+  //点击删除按钮,展示删除确认框
+  const onClickDeleteRow = (record?: any) => {
+    const postId =
+      record != undefined ? record.postId : selectedRowKeys.join(",");
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定删除岗位编号为“${postId}”的数据项?`,
+      onOk() {
+        executeDeleteRow(postId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的数据
+  const executeDeleteRow = async (postId: any) => {
+    const body = await fetchApi(`${deleteAPI}/${postId}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+
+        //修改按钮变回不可点击
+        setRowCanModify(false);
+        //删除按钮变回不可点击
+        setRowCanDelete(false);
+        //选中行数据重置为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionTableRef.current) {
+          actionTableRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //4.导出
+
+  //导出表格数据
+  const exportTable = async () => {
+    if (searchTableFormRef.current) {
+      const formData = new FormData();
+
+      const data = {
+        pageNum: page,
+        pageSize: pageSize,
+        ...searchTableFormRef.current.getFieldsValue(),
+      };
+
+      Object.keys(data).forEach((key) => {
+        if (data[key] !== undefined) {
+          formData.append(key, data[key]);
+        }
+      });
+
+      await fetchFile(
+        exportAPI,
+        push,
+        {
+          method: "POST",
+          body: formData,
+        },
+        `${exportFilePrefix}_${new Date().getTime()}.xlsx`
+      );
+    }
+  };
+
+  //5.选择行
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [selectedRow, setSelectedRow] = useState(undefined as any);
+
+  //修改按钮是否可用,选中行时才可用
+  const [rowCanModify, setRowCanModify] = useState(false);
+
+  //删除按钮是否可用,选中行时才可用
+  const [rowCanDelete, setRowCanDelete] = useState(false);
+
+  //ProTable rowSelection
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[], selectedRows: any[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+
+      if (newSelectedRowKeys && newSelectedRowKeys.length == 1) {
+        setSelectedRow(selectedRows[0]);
+        setRowCanModify(true);
+      } else {
+        setRowCanModify(false);
+        setSelectedRow(undefined);
+      }
+    },
+
+    //复选框的额外禁用判断
+    // getCheckboxProps: (record) => ({
+    //   disabled: record.userId == 1,
+    // }),
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionTableRef = useRef<ActionType>();
+  //搜索表单对象引用
+  const searchTableFormRef = useRef<ProFormInstance>();
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  return (
+    <PageContainer title={false}>
+      <ProTable
+        formRef={searchTableFormRef}
+        rowKey="postId"
+        rowSelection={{
+          selectedRowKeys,
+          ...rowSelection,
+        }}
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await queryTableData(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+          onChange: pageChange,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionTableRef}
+        toolbar={{
+          actions: [
+            <ModalForm
+              key="addmodal"
+              title="添加岗位"
+              trigger={
+                <Button icon={<PlusOutlined />} type="primary">
+                  新建
+                </Button>
+              }
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+              }}
+              submitTimeout={2000}
+              onFinish={executeAddData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="postName"
+                  label="岗位名称"
+                  placeholder="请输入岗位名称"
+                  rules={[{ required: true, message: "请输入岗位名称" }]}
+                />
+                <ProFormText
+                  width="md"
+                  name="postCode"
+                  label="岗位编码"
+                  placeholder="请输入岗位编码"
+                  rules={[{ required: true, message: "请输入岗位编码" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormDigit
+                  fieldProps={{ precision: 0 }}
+                  width="md"
+                  name="postSort"
+                  initialValue="0"
+                  label="岗位排序"
+                  placeholder="请输入岗位排序"
+                  rules={[{ required: true, message: "请输入岗位排序" }]}
+                />
+                <ProFormRadio.Group
+                  name="status"
+                  width="sm"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "停用",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProFormTextArea
+                name="remark"
+                width={688}
+                label="备注"
+                placeholder="请输入内容"
+              />
+            </ModalForm>,
+            <ModalForm
+              key="modifymodal"
+              title="修改岗位"
+              formRef={modifyFormRef}
+              trigger={
+                <Button
+                  icon={<FontAwesomeIcon icon={faPenToSquare} />}
+                  disabled={!rowCanModify}
+                  onClick={() => onClickShowRowModifyModal()}
+                >
+                  修改
+                </Button>
+              }
+              open={isShowModifyDataModal}
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+                onCancel: () => {
+                  setIsShowModifyDataModal(false);
+                },
+              }}
+              submitTimeout={2000}
+              onFinish={executeModifyData}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="postName"
+                  label="岗位名称"
+                  placeholder="请输入岗位名称"
+                  rules={[{ required: true, message: "请输入岗位名称" }]}
+                />
+                <ProFormText
+                  width="md"
+                  name="postCode"
+                  label="岗位编码"
+                  placeholder="请输入岗位编码"
+                  rules={[{ required: true, message: "请输入岗位编码" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormDigit
+                  fieldProps={{ precision: 0 }}
+                  width="md"
+                  name="postSort"
+                  initialValue="0"
+                  label="岗位排序"
+                  placeholder="请输入岗位排序"
+                  rules={[{ required: true, message: "请输入岗位排序" }]}
+                />
+                <ProFormRadio.Group
+                  name="status"
+                  width="sm"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "停用",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProFormTextArea
+                name="remark"
+                width={688}
+                label="备注"
+                placeholder="请输入内容"
+              />
+            </ModalForm>,
+
+            <Button
+              key="danger"
+              danger
+              icon={<DeleteOutlined />}
+              disabled={!rowCanDelete}
+              onClick={() => onClickDeleteRow()}
+            >
+              删除
+            </Button>,
+            <Button
+              key="export"
+              type="primary"
+              icon={<FontAwesomeIcon icon={faDownload} />}
+              onClick={exportTable}
+            >
+              导出
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionTableRef.current) {
+                  actionTableRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+    </PageContainer>
+  );
+}

+ 616 - 0
app/(business)/system/role/auth/[roleid]/page.tsx

@@ -0,0 +1,616 @@
+"use client";
+
+import { fetchApi, fetchFile } from "@/app/_modules/func";
+import {
+  CaretDownOutlined,
+  CheckOutlined,
+  CloseOutlined,
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  EyeOutlined,
+  PlusOutlined,
+  ReloadOutlined,
+  SearchOutlined,
+  KeyOutlined,
+  LoadingOutlined,
+  CloudUploadOutlined,
+  FileAddOutlined,
+} from "@ant-design/icons";
+import type {
+  ProColumns,
+  ProFormInstance,
+  ActionType,
+} from "@ant-design/pro-components";
+import {
+  ModalForm,
+  PageContainer,
+  ProCard,
+  ProForm,
+  ProFormRadio,
+  ProFormSelect,
+  ProFormText,
+  ProFormTextArea,
+  ProFormTreeSelect,
+  ProTable,
+} from "@ant-design/pro-components";
+
+import type { TreeDataNode, MenuProps, UploadProps, GetProp } from "antd";
+import {
+  Button,
+  Col,
+  Flex,
+  Input,
+  message,
+  Modal,
+  Row,
+  Space,
+  Spin,
+  Switch,
+  Tree,
+  Dropdown,
+  Form,
+  Upload,
+  Typography,
+  Checkbox,
+  Tag,
+} from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faDownload,
+  faPenToSquare,
+  faToggleOff,
+  faToggleOn,
+  faUpload,
+  faUsers,
+  faCheck,
+  faXmark,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useEffect, useMemo, useRef, useState } from "react";
+
+type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];
+
+const { Dragger } = Upload;
+
+export type OptionType = {
+  label: string;
+  value: string | number;
+};
+
+export default function RoleAuth({ params }: { params: { roleid: string } }) {
+  const { push } = useRouter();
+
+  const roleId = params.roleid;
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "用户名称",
+      dataIndex: "userName",
+      order: 2,
+    },
+    {
+      title: "用户昵称",
+      dataIndex: "nickName",
+      search: false,
+    },
+
+    {
+      title: "邮箱",
+      dataIndex: "email",
+      search: false,
+    },
+    {
+      title: "手机号",
+      dataIndex: "phonenumber",
+      order: 1,
+    },
+    {
+      title: "状态",
+      dataIndex: "status",
+      search: false,
+      valueEnum: {
+        0: {
+          text: "正常",
+          status: "0",
+        },
+        1: {
+          text: "停用",
+          status: "1",
+        },
+      },
+      render: (text, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.status == 0 ? "green" : "red"}
+              icon={
+                record.status == 0 ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {text}
+            </Tag>
+          </Space>
+        );
+      },
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      search: false,
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => {
+        if (record.userId != 1)
+          return [
+            <Button
+              key="deleteBtn"
+              type="link"
+              danger
+              icon={<DeleteOutlined />}
+              onClick={() => onClickRemoveAuth(record)}
+            >
+              取消授权
+            </Button>,
+          ];
+      },
+    },
+  ];
+
+  //未分配授权用户列定义
+  const unAllocateColumns: ProColumns[] = [
+    {
+      title: "用户名称",
+      dataIndex: "userName",
+      order: 2,
+    },
+    {
+      title: "用户昵称",
+      dataIndex: "nickName",
+      search: false,
+    },
+
+    {
+      title: "邮箱",
+      dataIndex: "email",
+      search: false,
+    },
+    {
+      title: "手机号",
+      dataIndex: "phonenumber",
+      order: 1,
+    },
+    {
+      title: "状态",
+      dataIndex: "status",
+      search: false,
+      valueEnum: {
+        0: {
+          text: "正常",
+          status: "0",
+        },
+        1: {
+          text: "停用",
+          status: "1",
+        },
+      },
+      render: (text, record) => {
+        return (
+          <Space>
+            <Tag
+              color={record.status == 0 ? "green" : "red"}
+              icon={
+                record.status == 0 ? (
+                  <FontAwesomeIcon icon={faCheck} />
+                ) : (
+                  <FontAwesomeIcon icon={faXmark} />
+                )
+              }
+            >
+              {text}
+            </Tag>
+          </Space>
+        );
+      },
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      search: false,
+    },
+  ];
+
+  //查询角色授权数据
+  const getRoleAllocate = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      roleId: roleId,
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(
+      `/api/system/role/authUser/allocatedList?${queryParams}`,
+      push
+    );
+
+    if (body !== undefined) {
+      return body;
+    }
+  };
+
+  //查询角色未授权数据
+  const getRoleUnallocate = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      roleId: roleId,
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(
+      `/api/system/role/authUser/unallocatedList?${queryParams}`,
+      push
+    );
+
+    if (body !== undefined) {
+      return body;
+    }
+  };
+
+  //取消授权按钮是否可用,选中行时才可用
+  const [rowCanRemoveAuth, setCanRemoveAuth] = useState(false);
+
+  //点击批量取消授权按钮
+  const onClickBatchRemoveAuth = () => {
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定要取消选中用户的角色授权吗?`,
+      onOk() {
+        executeBatchRemoveRoleAuth();
+      },
+      onCancel() {},
+    });
+  };
+
+  //点击取消授权按钮
+  const onClickRemoveAuth = (record: any) => {
+    const userId = record.userId;
+
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定要取消用户“${record.userName}”的角色授权吗?`,
+      onOk() {
+        executeRemoveRoleAuth(userId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //执行批量取消用户角色授权
+  const executeBatchRemoveRoleAuth = async () => {
+    const data = {
+      roleId: roleId,
+      userIds: selectedRowKeys.join(","),
+    };
+
+    const body = await fetchApi(
+      `/api/system/role/authUser/cancelAll?${new URLSearchParams(data)}`,
+      push,
+      {
+        method: "PUT",
+      }
+    );
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("批量取消授权成功");
+      } else {
+        message.error(body.msg);
+      }
+
+      setSelectedRowKeys([]);
+      //刷新表格
+      if (actionRef.current) {
+        actionRef.current.reload();
+      }
+    }
+  };
+
+  //执行取消用户角色授权
+  const executeRemoveRoleAuth = async (userId: any) => {
+    const data = {
+      roleId: roleId,
+      userId: userId,
+    };
+
+    const body = await fetchApi("/api/system/role/authUser/cancel", push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(data),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("取消授权成功");
+      } else {
+        message.error(body.msg);
+      }
+
+      //刷新表格
+      if (actionRef.current) {
+        actionRef.current.reload();
+      }
+    }
+  };
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setCanRemoveAuth(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+    },
+  };
+
+  //未授权用户选中行操作
+  const [selectedRowKeysUnallocate, setSelectedRowKeysUnallocate] = useState<
+    React.Key[]
+  >([]);
+
+  const rowSelectionUnallocate = {
+    onChange: (newSelectedRowKeys: React.Key[]) => {
+      setSelectedRowKeysUnallocate(newSelectedRowKeys);
+    },
+  };
+
+  //是否展示分配用户对话框
+  const [showUnallocateModal, setShowUnallocateModal] = useState(false);
+
+  //展示分配用户对话框
+  const onClickShowModal = () => {
+    if (unallocateActionRef.current) {
+      unallocateActionRef.current.reload();
+    }
+
+    setShowUnallocateModal(true);
+  };
+
+  //确认分配新的用户
+  const confirmAddUnallocate = async () => {
+    const data = {
+      roleId: roleId,
+      userIds: selectedRowKeysUnallocate.join(","),
+    };
+
+    const body = await fetchApi(
+      `/api/system/role/authUser/selectAll?${new URLSearchParams(data)}`,
+      push,
+      {
+        method: "PUT",
+      }
+    );
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+      } else {
+        message.error(body.msg);
+      }
+    }
+
+    setSelectedRowKeysUnallocate([]);
+
+    if (unallocateActionRef.current) {
+      unallocateActionRef.current.reload();
+    }
+
+    console.log(selectedRowKeysUnallocate);
+
+    if (actionRef.current) {
+      actionRef.current.reload();
+    }
+
+    setShowUnallocateModal(false);
+  };
+
+  //取消分配用户
+  const cancelAddUnallocate = () => {
+    setShowUnallocateModal(false);
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionRef = useRef<ActionType>();
+  //表单对象引用
+  const formRef = useRef<ProFormInstance>();
+
+  //未分配用户列表action对象引用
+  const unallocateActionRef = useRef<ActionType>();
+
+  //当前默认条数
+  const defaultPageSize = 10;
+
+  return (
+    <PageContainer
+      header={{
+        title: "分配用户",
+        onBack(e) {
+          push("/system/role");
+        },
+      }}
+    >
+      <ProTable
+        formRef={formRef}
+        rowKey="userId"
+        rowSelection={{
+          selectedRowKeys,
+          ...rowSelection,
+        }}
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await getRoleAllocate(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionRef}
+        toolbar={{
+          actions: [
+            <Button icon={<PlusOutlined />} key="allocate" type="primary" onClick={onClickShowModal}>
+              添加用户
+            </Button>,
+
+            <Button
+              key="unallocate"
+              danger
+              icon={<DeleteOutlined />}
+              disabled={!rowCanRemoveAuth}
+              onClick={() => onClickBatchRemoveAuth()}
+            >
+              批量取消授权
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionRef.current) {
+                  actionRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+      <Modal
+        title={`选择用户`}
+        width={1000}
+        open={showUnallocateModal}
+        onOk={confirmAddUnallocate}
+        onCancel={cancelAddUnallocate}
+      >
+        <ProTable
+          rowKey="userId"
+          rowSelection={{
+            selectedRowKeys: selectedRowKeysUnallocate,
+            ...rowSelectionUnallocate,
+          }}
+          columns={unAllocateColumns}
+          request={async (params: any, sorter: any, filter: any) => {
+            // 表单搜索项会从 params 传入,传递给后端接口。
+            const data = await getRoleUnallocate(params, sorter, filter);
+            if (data !== undefined) {
+              return Promise.resolve({
+                data: data.rows,
+                success: true,
+                total: data.total,
+              });
+            }
+            return Promise.resolve({
+              data: [],
+              success: true,
+            });
+          }}
+          pagination={{
+            defaultPageSize: defaultPageSize,
+            showQuickJumper: true,
+            showSizeChanger: true,
+          }}
+          search={
+            showSearch
+              ? {
+                  defaultCollapsed: false,
+                  searchText: "搜索",
+                }
+              : false
+          }
+          dateFormatter="string"
+          actionRef={unallocateActionRef}
+          toolbar={{
+            actions: [],
+            settings: [],
+          }}
+        />
+      </Modal>
+    </PageContainer>
+  );
+}

+ 998 - 0
app/(business)/system/role/page.tsx

@@ -0,0 +1,998 @@
+"use client";
+
+import { fetchApi, fetchFile } from "@/app/_modules/func";
+import {
+  CaretDownOutlined,
+  CheckOutlined,
+  CloseOutlined,
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  KeyOutlined,
+  PlusOutlined,
+  ReloadOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import {
+  ModalForm,
+  PageContainer,
+  ProForm,
+  ProFormDigit,
+  ProFormRadio,
+  ProFormText,
+  ProFormTextArea,
+  ProFormTreeSelect,
+  ProTable,
+} from "@ant-design/pro-components";
+import type { GetProp, UploadProps } from "antd";
+import {
+  Button,
+  Dropdown,
+  Form,
+  Input,
+  message,
+  Modal,
+  Select,
+  Space,
+  Switch,
+  Upload,
+} from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faDownload,
+  faPenToSquare,
+  faToggleOff,
+  faToggleOn,
+  faUsers,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { TreeSelect } from "@/node_modules/antd/es/index";
+import { useRef, useState } from "react";
+
+type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];
+
+const { Dragger } = Upload;
+
+export type OptionType = {
+  label: string;
+  value: string | number;
+};
+
+export default function Role() {
+  const { push } = useRouter();
+
+  //控制行的状态值的恢复
+  const [rowStatusMap, setRowStatusMap] = useState<{ [key: number]: boolean }>(
+    {}
+  );
+
+  //表格列定义
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "角色编号",
+      dataIndex: "roleId",
+      search: false,
+    },
+    {
+      title: "角色名称",
+      fieldProps: {
+        placeholder: "请输入角色名称",
+      },
+      dataIndex: "roleName",
+      ellipsis: true,
+      sorter: true,
+      order: 4,
+    },
+    {
+      title: "权限字符",
+      fieldProps: {
+        placeholder: "请输入权限字符",
+      },
+      dataIndex: "roleKey",
+      ellipsis: true,
+      sorter: true,
+      order: 3,
+    },
+    {
+      title: "角色排序",
+      dataIndex: "roleSort",
+      search: false,
+      sorter: true,
+    },
+    {
+      title: "状态",
+      fieldProps: {
+        placeholder: "请选择角色状态",
+      },
+      dataIndex: "status",
+      valueType: "select",
+      order: 2,
+      valueEnum: {
+        0: {
+          text: "正常",
+          status: "0",
+        },
+        1: {
+          text: "停用",
+          status: "1",
+        },
+      },
+      render: (text, record) => {
+        return (
+          <Space>
+            <Switch
+              checkedChildren={<CheckOutlined />}
+              unCheckedChildren={<CloseOutlined />}
+              defaultChecked={record.status === "0"}
+              checked={rowStatusMap[record.roleId]}
+              disabled={record.roleId == 1}
+              onChange={(checked, event) => {
+                showSwitchRoleStatusModal(checked, record);
+              }}
+            />
+          </Space>
+        );
+      },
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      search: false,
+      sorter: true,
+    },
+    {
+      title: "创建时间",
+      fieldProps: {
+        placeholder: ["开始日期", "结束日期"],
+      },
+      dataIndex: "createTimeRange",
+      valueType: "dateRange",
+      hideInTable: true,
+      order: 1,
+      search: {
+        transform: (value) => {
+          return {
+            "params[beginTime]": `${value[0]} 00:00:00`,
+            "params[endTime]": `${value[1]} 23:59:59`,
+          };
+        },
+      },
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => {
+        if (record.roleId != 1)
+          return [
+            <Button
+              key="modifyBtn"
+              type="link"
+              icon={<FontAwesomeIcon icon={faPenToSquare} />}
+              onClick={() => showRowModifyModal(record)}
+            >
+              修改
+            </Button>,
+            <Button
+              key="deleteBtn"
+              type="link"
+              danger
+              icon={<DeleteOutlined />}
+              onClick={() => onClickDeleteRow(record)}
+            >
+              删除
+            </Button>,
+            <Dropdown
+              key="moreDrop"
+              menu={{
+                items: [
+                  {
+                    key: "1",
+                    label: (
+                      <a
+                        onClick={() => {
+                          modifyRolePermission(record);
+                        }}
+                      >
+                        数据权限
+                      </a>
+                    ),
+                    icon: <KeyOutlined />,
+                  },
+                  {
+                    key: "2",
+                    label: (
+                      <a
+                        onClick={() =>
+                          push(`/system/role/auth/${record.roleId}`)
+                        }
+                      >
+                        分配用户
+                      </a>
+                    ),
+                    icon: <FontAwesomeIcon icon={faUsers} />,
+                  },
+                ],
+              }}
+            >
+              <a onClick={(e) => e.preventDefault()}>
+                <Space>
+                  更多
+                  <CaretDownOutlined />
+                </Space>
+              </a>
+            </Dropdown>,
+          ];
+      },
+    },
+  ];
+
+  //是否展示修改角色对话框
+  const [showModifyRoleModal, setShowModifyRoleModal] = useState(false);
+
+  //展示修改用户对话框
+  const showRowModifyModal = (record?: any) => {
+    queryRoleInfo(record);
+    setShowModifyRoleModal(true);
+    queryRolePermissionData(record);
+  };
+
+  //是否展示修改角色权限
+  const [showModifyRolePermissionModal, setShowModifyRolePermissionModal] =
+    useState(false);
+
+  //重置密码表单引用
+  const [scopeFormRef] = Form.useForm();
+
+  //打开修改角色权限范围对话框
+  const modifyRolePermission = (record: any) => {
+    attachRowdata["roleId"] = record.roleId;
+    attachRowdata["roleName"] = record.roleName;
+    setAttachRowdata(attachRowdata);
+    setShowModifyRolePermissionModal(true);
+
+    queryRoleScope(record.roleId);
+    queryRoleDeptTree(record.roleId);
+  };
+
+  //查询用户相关权限范围
+  const queryRoleScope = async (roleId: number) => {
+    const body = await fetchApi(`/api/system/role/${roleId}`, push);
+    if (body !== undefined) {
+      if (body.code == 200) {
+        scopeFormRef.setFieldsValue({
+          roleName: body.data.roleName,
+          roleKey: body.data.roleKey,
+          dataScope: body.data.dataScope,
+        });
+
+        if (body.data.dataScope === "2") {
+          setShowDept(true);
+        }
+      }
+    }
+  };
+
+  //角色权限范围的部门树
+  const [roleDeptTree, setRoleDeptTree] = useState([]);
+
+  //查询角色权限范围的部门树
+  const queryRoleDeptTree = async (roleId: number) => {
+    const body = await fetchApi(`/api/system/role/deptTree/${roleId}`, push);
+    if (body !== undefined) {
+      if (body.code == 200) {
+        setRoleDeptTree(body.depts);
+        scopeFormRef.setFieldsValue({
+          deptIds: body.checkedKeys,
+        });
+      }
+    }
+  };
+
+  //是否展示部门列表
+  const [showDept, setShowDept] = useState(false);
+
+  //选择权限范围
+  const onSelectScope = (value: number) => {
+    setShowDept(value == 2);
+  };
+
+  //确认修改角色权限范围
+  const confirmModifyRolePermission = () => {
+    scopeFormRef.submit();
+    setShowDept(false);
+  };
+
+  //取消修改角色权限范围
+  const cancelModifyRolePermission = () => {
+    setShowModifyRolePermissionModal(false);
+    setShowDept(false);
+  };
+
+  //执行修改分配权限范围
+  const executeModifyRolePermissionScope = async (values: any) => {
+    setShowModifyRolePermissionModal(false);
+    values["roleId"] = attachRowdata["roleId"];
+    if (!values.hasOwnProperty("deptIds")) {
+      values["deptIds"] = [];
+    }
+    console.log("depts:", values);
+    const body = await fetchApi("/api/system/role/dataScope", push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(`修改${attachRowdata["roleName"]}权限范围成功`);
+      } else {
+        message.error(body.msg);
+      }
+    }
+    scopeFormRef.resetFields();
+  };
+
+  //查询用户数据
+  const getList = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    const body = await fetchApi(`/api/system/role/list?${queryParams}`, push);
+
+    if (body !== undefined) {
+      body.rows.forEach((row: any) => {
+        setRowStatusMap({ ...rowStatusMap, [row.roleId]: row.status === "0" });
+      });
+    }
+
+    return body;
+  };
+
+  //展示切换角色状态对话框
+  const showSwitchRoleStatusModal = (checked: boolean, record: any) => {
+    setRowStatusMap({ ...rowStatusMap, [record.roleId]: checked });
+
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确认要${checked ? "启用" : "停用"}"${record.roleName}"角色吗?`,
+      onOk() {
+        executeSwitchStatus(checked, record.roleId, () => {
+          setRowStatusMap({ ...rowStatusMap, [record.roleId]: !checked });
+        });
+      },
+      onCancel() {
+        setRowStatusMap({ ...rowStatusMap, [record.roleId]: !checked });
+      },
+    });
+  };
+
+  //确认变更角色状态
+  const executeSwitchStatus = async (
+    checked: boolean,
+    roleId: string,
+    erroCallback: () => void
+  ) => {
+    const modifyData = {
+      roleId: roleId,
+      status: checked ? "0" : "1",
+    };
+    const body = await fetchApi(`/api/system/role/changeStatus`, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(modifyData),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+      } else {
+        message.error(body.msg);
+        erroCallback();
+      }
+    }
+  };
+
+  //删除按钮是否可用,选中行时才可用
+  const [rowCanDelete, setRowCanDelete] = useState(false);
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [selectedRow, setSelectedRow] = useState(undefined as any);
+
+  //修改按钮是否可用
+  const [rowCanModify, setRowCanModify] = useState(false);
+
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[], selectedRows: any[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+
+      if (newSelectedRowKeys && newSelectedRowKeys.length == 1) {
+        setSelectedRow(selectedRows[0]);
+        setRowCanModify(true);
+      } else {
+        setRowCanModify(false);
+        setSelectedRow(undefined);
+      }
+    },
+    getCheckboxProps: (record: any) => ({
+      disabled: record.roleId == 1,
+    }),
+  };
+
+  //确定新建角色
+  const executeAddRole = async (values: any) => {
+    const body = await fetchApi("/api/system/role", push, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+        return true;
+      }
+
+      message.error(body.msg);
+      return false;
+    }
+    return false;
+  };
+
+  //修改角色表单引用
+  const modifyFormRef = useRef<ProFormInstance>();
+
+  //操作角色的附加数据
+  const [attachRowdata, setAttachRowdata] = useState<{ [key: string]: any }>(
+    {}
+  );
+
+  //查询用户信息
+  const queryRoleInfo = async (record?: any) => {
+    const roleId = record !== undefined ? record.roleId : selectedRow.roleId;
+    const roleName =
+      record !== undefined ? record.roleName : selectedRow.roleName;
+
+    attachRowdata["roleId"] = roleId;
+    attachRowdata["roleName"] = roleName;
+
+    setAttachRowdata(attachRowdata);
+
+    if (roleId !== undefined) {
+      const body = await fetchApi(`/api/system/role/${roleId}`, push);
+
+      if (body !== undefined) {
+        if (body.code == 200) {
+          modifyFormRef?.current?.setFieldsValue({
+            roleName: body.data.roleName,
+            roleKey: body.data.roleKey,
+            roleSort: body.data.roleSort,
+            status: body.data.status,
+            menuIds: body.menuIds,
+            remark: body.data.remark,
+          });
+        }
+      }
+    }
+  };
+
+  //待修改角色选中的权限数据
+  const [roleSelectedPermission, setRoleSelectedPermission] = useState([]);
+
+  //修改角色框中权限树数据
+  const [rolePermissionTree, setRolePermissionTree] = useState([]);
+
+  //查询修改角色时权限树,并获取角色选中权限数据
+  const queryRolePermissionData = async (record?: any) => {
+    const roleId = record !== undefined ? record.roleId : selectedRow.roleId;
+
+    const body = await fetchApi(
+      `/api/system/menu/roleMenuTreeselect/${roleId}`,
+      push
+    );
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        setRolePermissionTree(body.menus);
+
+        //绑定角色已选择的权限
+        modifyFormRef?.current?.setFieldsValue({
+          menuIds: body.checkedKeys,
+        });
+      }
+    }
+  };
+
+  //确认修改角色
+  const executeModifyRole = async (values: any) => {
+    values["roleId"] = attachRowdata["roleId"];
+
+    const body = await fetchApi("/api/system/role", push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body !== undefined) {
+      setShowModifyRoleModal(false);
+      if (body.code == 200) {
+        message.success(body.msg);
+        //刷新列表
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+        return true;
+      }
+      message.error(body.msg);
+      return false;
+    }
+  };
+
+  //点击删除按钮
+  const onClickDeleteRow = (record?: any) => {
+    const roleId =
+      record != undefined ? record.roleId : selectedRowKeys.join(",");
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定删除角色编号为“${roleId}”的数据项?`,
+      onOk() {
+        executeDeleteRow(roleId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //确定删除选中的角色
+  const executeDeleteRow = async (roleId: any) => {
+    const body = await fetchApi(`/api/system/role/${roleId}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+
+        //修改按钮变回不可点击
+        setRowCanModify(false);
+        //删除按钮变回不可点击
+        setRowCanDelete(false);
+        //选中行数据重置为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionRef = useRef<ActionType>();
+  //表单对象引用
+  const formRef = useRef<ProFormInstance>();
+
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  //导出用户
+  const exportTable = async () => {
+    if (formRef.current) {
+      const formData = new FormData();
+
+      const data = {
+        pageNum: page,
+        pageSize: pageSize,
+        ...formRef.current.getFieldsValue(),
+      };
+
+      Object.keys(data).forEach((key) => {
+        if (data[key] !== undefined) {
+          formData.append(key, data[key]);
+        }
+      });
+
+      await fetchFile(
+        "/api/system/role/export",
+        push,
+        {
+          method: "POST",
+          body: formData,
+        },
+        `role_${new Date().getTime()}.xlsx`
+      );
+    }
+  };
+
+  //查询所有权限树
+  const getPermissionTree = async () => {
+    const body = await fetchApi("/api/system/menu/treeselect", push);
+    if (body !== undefined) {
+      if (body.code == 200) {
+        return body.data;
+      }
+    }
+
+    return [];
+  };
+
+  return (
+    <PageContainer title={false}>
+      <ProTable
+        formRef={formRef}
+        rowKey="roleId"
+        rowSelection={{
+          selectedRowKeys,
+          ...rowSelection,
+        }}
+        columns={columns}
+        request={async (params: any, sorter: any, filter: any) => {
+          // 表单搜索项会从 params 传入,传递给后端接口。
+          const data = await getList(params, sorter, filter);
+          if (data !== undefined) {
+            return Promise.resolve({
+              data: data.rows,
+              success: true,
+              total: data.total,
+            });
+          }
+          return Promise.resolve({
+            data: [],
+            success: true,
+          });
+        }}
+        pagination={{
+          defaultPageSize: defaultPageSize,
+          showQuickJumper: true,
+          showSizeChanger: true,
+          onChange: pageChange,
+        }}
+        search={
+          showSearch
+            ? {
+                defaultCollapsed: false,
+                searchText: "搜索",
+              }
+            : false
+        }
+        dateFormatter="string"
+        actionRef={actionRef}
+        toolbar={{
+          actions: [
+            <ModalForm
+              key="addmodal"
+              title="添加角色"
+              trigger={
+                <Button icon={<PlusOutlined />} type="primary">
+                  新建
+                </Button>
+              }
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+              }}
+              submitTimeout={2000}
+              onFinish={executeAddRole}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="roleName"
+                  label="角色名称"
+                  placeholder="请输入角色名称"
+                  rules={[{ required: true, message: "请输入角色名称" }]}
+                />
+                <ProFormText
+                  width="md"
+                  name="roleKey"
+                  label="权限字符"
+                  placeholder="请输入权限字符"
+                  rules={[{ required: true, message: "请输入权限字符" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormDigit
+                  fieldProps={{ precision: 0 }}
+                  width="md"
+                  name="roleSort"
+                  initialValue="0"
+                  label="角色排序"
+                  placeholder="请输入角色排序"
+                  rules={[{ required: true, message: "请输入角色排序" }]}
+                />
+                <ProFormRadio.Group
+                  name="status"
+                  width="sm"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "停用",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProFormTreeSelect
+                width="md"
+                name="menuIds"
+                label="菜单权限"
+                request={async () => {
+                  return getPermissionTree();
+                }}
+                fieldProps={{
+                  placement: "topRight",
+                  filterTreeNode: true,
+                  showSearch: true,
+                  multiple: true,
+                  treeCheckable: true,
+                  treeNodeFilterProp: "label",
+                  fieldNames: {
+                    label: "label",
+                    value: "id",
+                  },
+                }}
+              />
+              <ProFormTextArea
+                name="remark"
+                width={688}
+                label="备注"
+                placeholder="请输入内容"
+              />
+            </ModalForm>,
+            <ModalForm
+              key="modifymodal"
+              title="修改角色"
+              formRef={modifyFormRef}
+              trigger={
+                <Button
+                  icon={<FontAwesomeIcon icon={faPenToSquare} />}
+                  disabled={!rowCanModify}
+                  onClick={() => showRowModifyModal()}
+                >
+                  修改
+                </Button>
+              }
+              open={showModifyRoleModal}
+              autoFocusFirstInput
+              modalProps={{
+                destroyOnHidden: true,
+                onCancel: () => {
+                  setShowModifyRoleModal(false);
+                },
+              }}
+              submitTimeout={2000}
+              onFinish={executeModifyRole}
+            >
+              <ProForm.Group>
+                <ProFormText
+                  width="md"
+                  name="roleName"
+                  label="角色名称"
+                  placeholder="请输入角色名称"
+                  rules={[{ required: true, message: "请输入角色名称" }]}
+                />
+                <ProFormText
+                  width="md"
+                  name="roleKey"
+                  label="权限字符"
+                  placeholder="请输入权限字符"
+                  rules={[{ required: true, message: "请输入权限字符" }]}
+                />
+              </ProForm.Group>
+              <ProForm.Group>
+                <ProFormDigit
+                  fieldProps={{ precision: 0 }}
+                  width="md"
+                  name="roleSort"
+                  initialValue="0"
+                  label="角色排序"
+                  placeholder="请输入角色排序"
+                  rules={[{ required: true, message: "请输入角色排序" }]}
+                />
+                <ProFormRadio.Group
+                  name="status"
+                  width="sm"
+                  label="状态"
+                  initialValue="0"
+                  options={[
+                    {
+                      label: "正常",
+                      value: "0",
+                    },
+                    {
+                      label: "停用",
+                      value: "1",
+                    },
+                  ]}
+                />
+              </ProForm.Group>
+              <ProFormTreeSelect
+                width="md"
+                name="menuIds"
+                label="菜单权限"
+                initialValue={roleSelectedPermission}
+                request={async () => {
+                  return rolePermissionTree;
+                }}
+                fieldProps={{
+                  placement: "topRight",
+                  filterTreeNode: true,
+                  showSearch: true,
+                  multiple: true,
+                  treeCheckable: true,
+                  treeNodeFilterProp: "label",
+                  fieldNames: {
+                    label: "label",
+                    value: "id",
+                  },
+                }}
+              />
+              <ProFormTextArea
+                name="remark"
+                width={688}
+                label="备注"
+                placeholder="请输入内容"
+              />
+            </ModalForm>,
+
+            <Button
+              key="danger"
+              danger
+              icon={<DeleteOutlined />}
+              disabled={!rowCanDelete}
+              onClick={() => onClickDeleteRow()}
+            >
+              删除
+            </Button>,
+            <Button
+              key="export"
+              type="primary"
+              icon={<FontAwesomeIcon icon={faDownload} />}
+              onClick={exportTable}
+            >
+              导出
+            </Button>,
+          ],
+          settings: [
+            {
+              key: "switch",
+              icon: showSearch ? (
+                <FontAwesomeIcon icon={faToggleOn} />
+              ) : (
+                <FontAwesomeIcon icon={faToggleOff} />
+              ),
+              tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+              onClick: (key: string | undefined) => {
+                setShowSearch(!showSearch);
+              },
+            },
+            {
+              key: "refresh",
+              tooltip: "刷新",
+              icon: <ReloadOutlined />,
+              onClick: (key: string | undefined) => {
+                if (actionRef.current) {
+                  actionRef.current.reload();
+                }
+              },
+            },
+          ],
+        }}
+      />
+
+      <Modal
+        title={`分配数据权限`}
+        open={showModifyRolePermissionModal}
+        onOk={confirmModifyRolePermission}
+        onCancel={cancelModifyRolePermission}
+      >
+        <Form
+          layout="horizontal"
+          form={scopeFormRef}
+          onFinish={executeModifyRolePermissionScope}
+        >
+          <Form.Item name="roleName" label="角色名称">
+            <Input disabled />
+          </Form.Item>
+          <Form.Item name="roleKey" label="权限字符">
+            <Input disabled />
+          </Form.Item>
+
+          <Form.Item name="dataScope" label="权限范围" initialValue="1">
+            <Select
+              placeholder="选择权限范围"
+              onChange={onSelectScope}
+              options={[
+                {
+                  label: "全部数据权限",
+                  value: "1",
+                },
+                {
+                  label: "自定义数据权限",
+                  value: "2",
+                },
+                {
+                  label: "本部门数据权限",
+                  value: "3",
+                },
+                {
+                  label: "本部门及以下权限",
+                  value: "4",
+                },
+                {
+                  label: "仅本人数据权限",
+                  value: "5",
+                },
+              ]}
+            ></Select>
+          </Form.Item>
+          {showDept && (
+            <Form.Item name="deptIds" label="数据权限">
+              <TreeSelect
+                treeData={roleDeptTree}
+                allowClear
+                multiple={true}
+                showCheckedStrategy={TreeSelect.SHOW_ALL}
+                treeCheckable={true}
+                fieldNames={{
+                  label: "label",
+                  value: "id",
+                }}
+              />
+            </Form.Item>
+          )}
+        </Form>
+      </Modal>
+    </PageContainer>
+  );
+}

+ 184 - 0
app/(business)/system/user/auth/[userid]/page.tsx

@@ -0,0 +1,184 @@
+"use client";
+
+import {
+  PageContainer,
+  ProDescriptions,
+  ProTable,
+  ProCard,
+  ProForm,
+  ProFormText,
+  ProSkeleton,
+} from "@ant-design/pro-components";
+
+import type { ProColumns } from "@ant-design/pro-components";
+
+import { Divider, message, Flex, Button } from "antd";
+
+import { fetchApi } from "@/app/_modules/func";
+
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function UserAuth({ params }: { params: { userid: string } }) {
+  const { push } = useRouter();
+
+  //用户信息
+  const [user, setUser] = useState<any>({});
+  //角色信息
+  const [roles, setRoles] = useState([]);
+
+  const getUserData = async () => {
+    const body = await fetchApi(
+      `/api/system/user/authRole/${params.userid}`,
+      push
+    );
+    if (body !== undefined) {
+      if (body.code == 200) {
+        setUser(body.user);
+        setRoles(body.roles);
+
+        setSelectedRowKeys(body.user.roles.map((item: any) => item.roleId));
+      }
+    }
+  };
+
+  useEffect(() => {
+    getUserData();
+  }, []);
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "序号",
+      dataIndex: "index",
+      valueType: "indexBorder",
+      width: 48,
+    },
+    {
+      title: "角色编号",
+      dataIndex: "roleId",
+      search: false,
+    },
+    {
+      title: "角色名称",
+      dataIndex: "roleName",
+      search: false,
+    },
+    {
+      title: "权限字符",
+      dataIndex: "roleKey",
+      search: false,
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+    },
+  ];
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+    },
+  };
+
+  //当前每页条数
+  const defaultPageSize = 10;
+
+  //提交按钮加载状态
+  const [confirmLoding, setConfirmLoading] = useState(false);
+
+  //更新用户角色
+  const updateAuth = async () => {
+    setConfirmLoading(true);
+    const queryParam = {
+      userId: user.userId,
+      roleIds: selectedRowKeys.join(","),
+    };
+
+    const body = await fetchApi(
+      `/api/system/user/authRole?${new URLSearchParams(queryParam)}`,
+      push,
+      {
+        method: "PUT",
+      }
+    );
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("授权成功");
+      } else {
+        message.error(body.msg);
+      }
+    }
+
+    setConfirmLoading(false);
+  };
+
+  return (
+    <PageContainer
+      header={{
+        title: "分配角色",
+        onBack(e) {
+          push("/system/user");
+        },
+      }}
+    >
+      {Object.keys(user).length === 0 ? (
+        <ProSkeleton type="list" />
+      ) : (
+        <>
+          <ProCard title="基本信息">
+            <ProDescriptions column={24}>
+              <ProDescriptions.Item span={12} label="用户昵称">
+                {user.nickName}
+              </ProDescriptions.Item>
+              <ProDescriptions.Item span={12} label="用户名称">
+                {user.userName}
+              </ProDescriptions.Item>
+            </ProDescriptions>
+          </ProCard>
+          <Divider />
+          <ProCard title="角色信息">
+            <ProTable
+              rowKey="roleId"
+              rowSelection={{
+                selectedRowKeys,
+                ...rowSelection,
+              }}
+              tableAlertRender={false}
+              columns={columns}
+              dataSource={roles}
+              pagination={{
+                defaultPageSize: defaultPageSize,
+                showQuickJumper: true,
+                showSizeChanger: true,
+                // onChange: pageChange,
+              }}
+              search={false}
+              dateFormatter="string"
+              toolbar={{
+                actions: [],
+                settings: [],
+              }}
+            />
+          </ProCard>
+          <ProCard>
+            <Flex justify="center" gap="middle">
+              <Button href="/system/user">返回</Button>
+              <Button
+                type="primary"
+                onClick={updateAuth}
+                loading={confirmLoding}
+              >
+                提交
+              </Button>
+            </Flex>
+          </ProCard>
+        </>
+      )}
+    </PageContainer>
+  );
+}

+ 1350 - 0
app/(business)/system/user/page.tsx

@@ -0,0 +1,1350 @@
+"use client";
+
+import { fetchApi, fetchFile } from "@/app/_modules/func";
+import {
+  CaretDownOutlined,
+  CheckOutlined,
+  CloseOutlined,
+  DeleteOutlined,
+  ExclamationCircleFilled,
+  FileAddOutlined,
+  KeyOutlined,
+  LoadingOutlined,
+  PlusOutlined,
+  ReloadOutlined,
+  SearchOutlined,
+} from "@ant-design/icons";
+import type {
+  ActionType,
+  ProColumns,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import {
+  ModalForm,
+  PageContainer,
+  ProCard,
+  ProForm,
+  ProFormRadio,
+  ProFormSelect,
+  ProFormText,
+  ProFormTextArea,
+  ProFormTreeSelect,
+  ProTable,
+} from "@ant-design/pro-components";
+import type { GetProp, TreeDataNode, UploadProps } from "antd";
+import {
+  Button,
+  Checkbox,
+  Col,
+  Dropdown,
+  Flex,
+  Form,
+  Input,
+  message,
+  Modal,
+  Row,
+  Space,
+  Spin,
+  Switch,
+  Tree,
+  Typography,
+  Upload,
+} from "antd";
+import { useRouter } from "next/navigation";
+
+import {
+  faDownload,
+  faPenToSquare,
+  faToggleOff,
+  faToggleOn,
+  faUpload,
+  faUsers,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useEffect, useMemo, useRef, useState } from "react";
+
+type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];
+
+const { Dragger } = Upload;
+
+export type OptionType = {
+  label: string;
+  value: string | number;
+};
+
+export default function User() {
+  const { push } = useRouter();
+
+  //新建用户预置密码值
+  const [defaultPassword, setDefaultPassword] = useState("");
+
+  useEffect(() => {
+    queryDefaultPassword();
+    queryPostion();
+    queryOrgTree();
+  }, []);
+
+  //控制行的状态值的恢复
+  const [rowStatusMap, setRowStatusMap] = useState<{ [key: number]: boolean }>(
+    {}
+  );
+
+  //表格列定义
+  const columns: ProColumns[] = [
+    {
+      title: "用户编号",
+      dataIndex: "userId",
+      search: false,
+    },
+    {
+      title: "用户名称",
+      fieldProps: {
+        placeholder: "请输入用户名称",
+      },
+      dataIndex: "userName",
+      ellipsis: true,
+      sorter: true,
+      order: 4,
+    },
+    {
+      title: "用户昵称",
+      dataIndex: "nickName",
+      ellipsis: true,
+      sorter: true,
+      search: false,
+    },
+    {
+      title: "部门名称",
+      key: "deptName",
+      search: false,
+      render: (text, record) => record.dept?.deptName,
+    },
+
+    {
+      title: "手机号",
+      fieldProps: {
+        placeholder: "请输入手机号",
+      },
+      dataIndex: "phonenumber",
+      order: 3,
+    },
+    {
+      title: "状态",
+      fieldProps: {
+        placeholder: "请选择用户状态",
+      },
+      dataIndex: "status",
+      valueType: "select",
+      order: 2,
+      valueEnum: {
+        0: {
+          text: "正常",
+          status: "0",
+        },
+        1: {
+          text: "停用",
+          status: "1",
+        },
+      },
+      render: (text, record) => {
+        return (
+          <Space>
+            <Switch
+              checkedChildren={<CheckOutlined />}
+              unCheckedChildren={<CloseOutlined />}
+              defaultChecked={record.status === "0"}
+              checked={rowStatusMap[record.userId]}
+              disabled={record.userId == 1}
+              onChange={(checked, event) => {
+                showSwitchUserStatusModal(checked, record);
+              }}
+            />
+          </Space>
+        );
+      },
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createTime",
+      valueType: "dateTime",
+      search: false,
+      sorter: true,
+    },
+    {
+      title: "创建时间",
+      fieldProps: {
+        placeholder: ["开始日期", "结束日期"],
+      },
+      dataIndex: "createTimeRange",
+      valueType: "dateRange",
+      hideInTable: true,
+      order: 1,
+      search: {
+        transform: (value) => {
+          return {
+            "params[beginTime]": `${value[0]} 00:00:00`,
+            "params[endTime]": `${value[1]} 23:59:59`,
+          };
+        },
+      },
+    },
+    {
+      title: "操作",
+      key: "option",
+      search: false,
+      render: (_, record) => {
+        if (record.userId != 1)
+          return [
+            <Button
+              key="modifyBtn"
+              type="link"
+              icon={<FontAwesomeIcon icon={faPenToSquare} />}
+              onClick={() => showRowModifyModal(record)}
+            >
+              修改
+            </Button>,
+            <Button
+              key="deleteBtn"
+              type="link"
+              danger
+              icon={<DeleteOutlined />}
+              onClick={() => onClickDeleteRow(record)}
+            >
+              删除
+            </Button>,
+            <Dropdown
+              key="moreDrop"
+              menu={{
+                items: [
+                  {
+                    key: "1",
+                    label: (
+                      <a
+                        onClick={() => {
+                          modifyUserPwd(record);
+                        }}
+                      >
+                        重置密码
+                      </a>
+                    ),
+                    icon: <KeyOutlined />,
+                  },
+                  {
+                    key: "2",
+                    label: (
+                      <a
+                        onClick={() =>
+                          push(`/system/user/auth/${record.userId}`)
+                        }
+                      >
+                        分配角色
+                      </a>
+                    ),
+                    icon: <FontAwesomeIcon icon={faUsers} />,
+                  },
+                ],
+              }}
+            >
+              <a onClick={(e) => e.preventDefault()}>
+                <Space>
+                  更多
+                  <CaretDownOutlined />
+                </Space>
+              </a>
+            </Dropdown>,
+          ];
+      },
+    },
+  ];
+
+  //是否展示修改用户对话框
+  const [showModifyUserModal, setShowModifyUserModal] = useState(false);
+
+  //展示修改用户对话框
+  const showRowModifyModal = (record?: any) => {
+    queryUserInfo(record);
+    setShowModifyUserModal(true);
+  };
+
+  //是否展示修改密码
+  const [showModifyUserPwdModal, setShowModifyUserPwdModal] = useState(false);
+
+  //重置密码表单引用
+  const [pwdFormRef] = Form.useForm();
+
+  const modifyUserPwd = (record: any) => {
+    attachUserdata["userId"] = record.userId;
+    attachUserdata["userName"] = record.userName;
+    setAttachUserdata(attachUserdata);
+
+    setShowModifyUserPwdModal(true);
+  };
+
+  //确认重置密码
+  const confirmModifyUserPwd = () => {
+    pwdFormRef.submit();
+  };
+
+  //取消重置密码
+  const cancelModifyUserPwd = () => {
+    setShowModifyUserPwdModal(false);
+  };
+
+  //执行重置密码
+  const executeModifyUserPwd = async (values: any) => {
+    setShowModifyUserPwdModal(false);
+    values["userId"] = attachUserdata["userId"];
+    const body = await fetchApi("/api/system/user/resetPwd", push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(`修改${attachUserdata["userName"]}密码成功`);
+      } else {
+        message.error(body.msg);
+      }
+    }
+    pwdFormRef.resetFields();
+  };
+
+  //查询用户数据
+  const getUser = async (params: any, sorter: any, filter: any) => {
+    const searchParams = {
+      pageNum: params.current,
+      ...params,
+    };
+
+    delete searchParams.current;
+
+    const queryParams = new URLSearchParams(searchParams);
+
+    Object.keys(sorter).forEach((key) => {
+      queryParams.append("orderByColumn", key);
+      if (sorter[key] === "ascend") {
+        queryParams.append("isAsc", "ascending");
+      } else {
+        queryParams.append("isAsc", "descending");
+      }
+    });
+
+    //如果有组织id,添加相应查询参数
+    if (searchDeptId != 0) {
+      queryParams.append("deptId", searchDeptId.toString());
+    }
+
+    const body = await fetchApi(`/api/system/user/list?${queryParams}`, push);
+
+    if (body !== undefined) {
+      body.rows.forEach((row: any) => {
+        setRowStatusMap({ ...rowStatusMap, [row.userId]: row.status === "0" });
+      });
+    }
+
+    return body;
+  };
+
+  //展示切换用户状态对话框
+  const showSwitchUserStatusModal = (checked: boolean, record: any) => {
+    setRowStatusMap({ ...rowStatusMap, [record.userId]: checked });
+
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确认要${checked ? "启用" : "停用"}"${record.userName}"用户吗?`,
+      onOk() {
+        executeSwitchStatus(checked, record.userId, () => {
+          setRowStatusMap({ ...rowStatusMap, [record.userId]: !checked });
+        });
+      },
+      onCancel() {
+        setRowStatusMap({ ...rowStatusMap, [record.userId]: !checked });
+      },
+    });
+  };
+
+  //确认变更用户状态
+  const executeSwitchStatus = async (
+    checked: boolean,
+    userId: string,
+    erroCallback: () => void
+  ) => {
+    const modifyData = {
+      userId: userId,
+      status: checked ? "0" : "1",
+    };
+    const body = await fetchApi(`/api/system/user/changeStatus`, push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(modifyData),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+      } else {
+        message.error(body.msg);
+        erroCallback();
+      }
+    }
+  };
+
+  //删除按钮是否可用,选中行时才可用
+  const [rowCanDelete, setRowCanDelete] = useState(false);
+
+  //选中行操作
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [selectedRow, setSelectedRow] = useState(undefined as any);
+
+  //修改按钮是否可用
+  const [rowCanModify, setRowCanModify] = useState(false);
+
+  const rowSelection = {
+    onChange: (newSelectedRowKeys: React.Key[], selectedRows: any[]) => {
+      setSelectedRowKeys(newSelectedRowKeys);
+      setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
+
+      if (newSelectedRowKeys && newSelectedRowKeys.length == 1) {
+        setSelectedRow(selectedRows[0]);
+        setRowCanModify(true);
+      } else {
+        setRowCanModify(false);
+        setSelectedRow(undefined);
+      }
+    },
+    getCheckboxProps: (record: any) => ({
+      disabled: record.userId == 1,
+    }),
+  };
+
+  //查询用的组织id
+  const [searchDeptId, setSearchDeptId] = useState(0);
+
+  //选择组织树执行过滤
+  const selectOrgData = (selectedDeptKey: React.Key[]) => {
+    if (selectedDeptKey && selectedDeptKey.length > 0) {
+      setSearchDeptId(selectedDeptKey[0] as number);
+    } else {
+      setSearchDeptId(0);
+    }
+
+    if (formRef.current) {
+      formRef.current.submit();
+    }
+  };
+
+  //用于搜索的组织选择数据
+  const [orgTreeData, setOrgTreeData] = useState([] as Array<TreeDataNode>);
+
+  //用于对话框的组织选择数据
+  const [orgSelectData, setOrgSelectData] = useState([]);
+
+  //查询组织树
+  const queryOrgTree = async () => {
+    const body = await fetchApi("/api/system/user/deptTree", push);
+    if (body !== undefined) {
+      setOrgTreeData(generateOrgTree(body.data));
+      setSearchValue("");
+      setOrgSelectData(body.data);
+    }
+  };
+
+  //搜索部门的值
+  const [searchValue, setSearchValue] = useState("");
+
+  //搜索组织树数据
+  const onSearchDept = (e: React.ChangeEvent<HTMLInputElement>) => {
+    setSearchValue(e.target.value);
+  };
+
+  //搜索过滤后的组织树展示数据
+  const filterOrgTree = useMemo(() => {
+    const loop = (data: TreeDataNode[]): TreeDataNode[] =>
+      data.map((item) => {
+        const strTitle = item.title as string;
+        const index = strTitle.indexOf(searchValue);
+        const beforeStr = strTitle.substring(0, index);
+        const afterStr = strTitle.slice(index + searchValue.length);
+        const title =
+          index > -1 ? (
+            <span>
+              {beforeStr}
+              <span style={{ color: "#f50" }}>{searchValue}</span>
+              {afterStr}
+            </span>
+          ) : (
+            <span>{strTitle}</span>
+          );
+        if (item.children) {
+          return { title, key: item.key, children: loop(item.children) };
+        }
+
+        return {
+          title,
+          key: item.key,
+        };
+      });
+
+    const data = loop(orgTreeData);
+    return data;
+  }, [orgTreeData, searchValue]);
+
+  const generateOrgTree = (orgData: []) => {
+    const children: Array<TreeDataNode> = new Array<TreeDataNode>();
+
+    orgData.forEach((parent: any) => {
+      const hasChild = parent.children && parent.children.length > 0;
+      const node: TreeDataNode = {
+        title: parent.label,
+        key: parent.id,
+      };
+
+      children.push(node);
+
+      if (hasChild) {
+        generateOrgChildTree(parent.children, node);
+      }
+    });
+    return children;
+  };
+
+  const generateOrgChildTree = (orgData: [], parent: TreeDataNode) => {
+    const children: Array<TreeDataNode> = new Array<TreeDataNode>();
+    orgData.forEach((item: any) => {
+      const hasChild = item.children && item.children.length > 0;
+      const node: TreeDataNode = {
+        title: item.label,
+        key: item.id,
+        isLeaf: !hasChild,
+      };
+
+      children.push(node);
+
+      if (hasChild) {
+        generateOrgChildTree(item.children, node);
+      }
+    });
+
+    parent.children = children;
+    return parent;
+  };
+
+  //查询性别分类
+  const querySexType = async () => {
+    const body = await fetchApi(
+      "/api/system/dict/data/type/sys_user_sex",
+      push
+    );
+    if (body !== undefined) {
+      return body.data;
+    }
+  };
+
+  //查询新建用户预置密码
+  const queryDefaultPassword = async () => {
+    const body = await fetchApi(
+      "/api/system/config/configKey/sys.user.initPassword",
+      push
+    );
+    if (body !== undefined) {
+      setDefaultPassword(body.msg);
+    }
+  };
+
+  //岗位数据
+  const [positionValue, setPositionValue] = useState<{ [key: number]: string }>(
+    {}
+  );
+
+  //角色数据
+  const [roleValue, setRoleValue] = useState<{ [key: number]: string }>({});
+
+  //查询岗位
+  const queryPostion = async () => {
+    const body = await fetchApi("/api/system/user/", push);
+
+    if (body !== undefined) {
+      body.posts.forEach((post: any) => {
+        positionValue[post.postId] = post.postName;
+        setPositionValue(positionValue);
+      });
+      body.roles.forEach((role: any) => {
+        roleValue[role.roleId] = role.roleName;
+        setRoleValue(roleValue);
+      });
+    }
+  };
+
+  //确定新建用户
+  const executeAddUser = async (values: any) => {
+    const body = await fetchApi("/api/system/user", push, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body != undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+        return true;
+      }
+
+      message.error(body.msg);
+      return false;
+    }
+    return false;
+  };
+
+  //修改用户表单引用
+  const modifyFormRef = useRef<ProFormInstance>();
+
+  //待修改用户的岗位可选数据
+  const [modifyPositionValue, setModifyPositionValue] = useState(
+    [] as Array<OptionType>
+  );
+  //待修改用户的角色可选数据
+  const [modifyRoleValue, setModifyRoleValue] = useState(
+    [] as Array<OptionType>
+  );
+
+  //操作用户的附加数据
+  const [attachUserdata, setAttachUserdata] = useState<{ [key: string]: any }>(
+    {}
+  );
+
+  //查询用户信息
+  const queryUserInfo = async (record?: any) => {
+    const userId = record !== undefined ? record.userId : selectedRow.userId;
+    const userName =
+      record !== undefined ? record.userName : selectedRow.userName;
+
+    attachUserdata["userId"] = userId;
+    attachUserdata["userName"] = userName;
+
+    setAttachUserdata(attachUserdata);
+
+    if (userId !== undefined) {
+      const body = await fetchApi(`/api/system/user/${userId}`, push);
+
+      if (body !== undefined) {
+        if (body.code == 200) {
+          const positionArray: Array<OptionType> = new Array<OptionType>();
+          body.posts.forEach((post: any) => {
+            const option: OptionType = {
+              label: post.postName,
+              value: post.postId,
+            };
+            positionArray.push(option);
+          });
+
+          setModifyPositionValue(positionArray);
+
+          const roeArray: Array<OptionType> = new Array<OptionType>();
+          body.roles.forEach((role: any) => {
+            const option: OptionType = {
+              label: role.roleName,
+              value: role.roleId,
+            };
+            roeArray.push(option);
+          });
+
+          setModifyRoleValue(roeArray);
+
+          modifyFormRef?.current?.setFieldsValue({
+            nickName: body.data.nickName,
+            deptId: body.data.deptId,
+            phonenumber: body.data.phonenumber,
+            email: body.data.email,
+            sex: body.data.sex,
+            status: body.data.status,
+            postIds: body.postIds,
+            roleIds: body.roleIds,
+            remark: body.data.remark,
+          });
+        }
+      }
+    }
+  };
+
+  //确认修改用户
+  const executeModifyUser = async (values: any) => {
+    values["userId"] = attachUserdata["userId"];
+    values["userName"] = attachUserdata["userName"];
+
+    const body = await fetchApi("/api/system/user", push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success(body.msg);
+        //刷新列表
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+        return true;
+      }
+      message.error(body.msg);
+      return false;
+    }
+  };
+
+  //点击删除按钮
+  const onClickDeleteRow = (record?: any) => {
+    const userId =
+      record != undefined ? record.userId : selectedRowKeys.join(",");
+    Modal.confirm({
+      title: "系统提示",
+      icon: <ExclamationCircleFilled />,
+      content: `确定删除用户编号为“${userId}”的数据项?`,
+      onOk() {
+        executeDeleteRow(userId);
+      },
+      onCancel() {},
+    });
+  };
+
+  //选中上传文件列表
+  const [fileList, setFileList] = useState<FileType[]>([]);
+
+  //上传前检查
+  const beforeUpload = (file: FileType) => {
+    setFileList([file]);
+    const isExcel =
+      file.type ===
+      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+    if (!isExcel) {
+      message.error("请上传 xls、xlsx 格式文件!");
+      setFileList([]);
+    }
+    return false;
+  };
+
+  //移除待上传文件
+  const removeFile = () => {
+    setFileList([]);
+  };
+
+  //上传文件是否刷新已有用户数据
+  const [uploadSupport, setUploadSupport] = useState(false);
+
+  //文件上传状态
+  const [uploading, setUploading] = useState(false);
+
+  //上传处理,手动上传下不会执行
+  const handleChange: UploadProps["onChange"] = (info: any) => {
+    if (info.file.status === "uploading") {
+      setUploading(true);
+      return;
+    }
+    if (info.file.status === "done") {
+      setUploading(false);
+      console.log(info.file.response);
+      if (info.file.response.code == 200) {
+        message.success(info.file.response.msg);
+      } else {
+        message.error(info.file.response.msg);
+      }
+    }
+  };
+
+  //导入对话框是否展示
+  const [showImportModal, setShowImportModal] = useState(false);
+
+  //点击导入按钮
+  const onClickImport = () => {
+    setShowImportModal(true);
+  };
+
+  //确定删除选中的用户
+  const executeDeleteRow = async (userId: any) => {
+    const body = await fetchApi(`/api/system/user/${userId}`, push, {
+      method: "DELETE",
+    });
+    if (body !== undefined) {
+      if (body.code == 200) {
+        message.success("删除成功");
+
+        //修改按钮变回不可点击
+        setRowCanModify(false);
+        //删除按钮变回不可点击
+        setRowCanDelete(false);
+        //选中行数据重置为空
+        setSelectedRowKeys([]);
+        //刷新列表
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //确定导入
+  const executeImport = async () => {
+    if (fileList.length == 0) {
+      message.error("请选择上传的文件");
+      return;
+    }
+
+    setUploading(true);
+
+    const file = fileList[0];
+    const formData = new FormData();
+    formData.append("file", file);
+    const body = await fetchApi(
+      `/api/system/user/importData?updateSupport=${uploadSupport}`,
+      push,
+      {
+        method: "POST",
+        body: formData,
+      }
+    );
+
+    setUploading(false);
+    setUploadSupport(false);
+
+    if (body !== undefined) {
+      setFileList([]);
+      if (body.code == 200) {
+        message.success("用户导入成功");
+        //刷新列表
+        if (actionRef.current) {
+          actionRef.current.reload();
+        }
+      } else {
+        message.error(body.msg);
+      }
+    }
+  };
+
+  //取消导入对话框
+  const cancelImportModal = () => {
+    setShowImportModal(false);
+    setUploadSupport(false);
+    setFileList([]);
+  };
+
+  //搜索栏显示状态
+  const [showSearch, setShowSearch] = useState(true);
+  //action对象引用
+  const actionRef = useRef<ActionType>();
+  //表单对象引用
+  const formRef = useRef<ProFormInstance>();
+
+  //当前页数和每页条数
+  const [page, setPage] = useState(1);
+  const defaultPageSize = 10;
+  const [pageSize, setPageSize] = useState(defaultPageSize);
+
+  const pageChange = (page: number, pageSize: number) => {
+    setPage(page);
+    setPageSize(pageSize);
+  };
+
+  //导出用户
+  const exportTable = async () => {
+    if (formRef.current) {
+      const formData = new FormData();
+
+      const data = {
+        pageNum: page,
+        pageSize: pageSize,
+        ...formRef.current.getFieldsValue(),
+      };
+
+      Object.keys(data).forEach((key) => {
+        if (data[key] !== undefined) {
+          formData.append(key, data[key]);
+        }
+      });
+
+      await fetchFile(
+        "/api/system/user/export",
+        push,
+        {
+          method: "POST",
+          body: formData,
+        },
+        `user_${new Date().getTime()}.xlsx`
+      );
+    }
+  };
+
+  return (
+    <PageContainer title={false}>
+      <Row gutter={{ xs: 8, sm: 8, md: 8 }}>
+        <Col xs={24} sm={6} md={6}>
+          <ProCard>
+            <Input
+              style={{ marginBottom: 16 }}
+              placeholder="输入部门名称搜索"
+              prefix={<SearchOutlined />}
+              onChange={onSearchDept}
+            />
+            {filterOrgTree.length > 0 ? (
+              <Flex>
+                <Tree
+                  switcherIcon={<CaretDownOutlined />}
+                  defaultExpandAll
+                  onSelect={selectOrgData}
+                  treeData={filterOrgTree}
+                />
+              </Flex>
+            ) : (
+              <Flex justify="center" style={{ marginTop: "16px" }}>
+                <Spin />
+              </Flex>
+            )}
+          </ProCard>
+        </Col>
+        <Col xs={24} sm={18} md={18}>
+          <ProTable
+            formRef={formRef}
+            rowKey="userId"
+            rowSelection={{
+              selectedRowKeys,
+              ...rowSelection,
+            }}
+            columns={columns}
+            request={async (params: any, sorter: any, filter: any) => {
+              // 表单搜索项会从 params 传入,传递给后端接口。
+              const data = await getUser(params, sorter, filter);
+              if (data !== undefined) {
+                return Promise.resolve({
+                  data: data.rows,
+                  success: true,
+                  total: data.total,
+                });
+              }
+              return Promise.resolve({
+                data: [],
+                success: true,
+              });
+            }}
+            pagination={{
+              defaultPageSize: defaultPageSize,
+              showQuickJumper: true,
+              showSizeChanger: true,
+              onChange: pageChange,
+            }}
+            search={
+              showSearch
+                ? {
+                    defaultCollapsed: false,
+                    searchText: "搜索",
+                  }
+                : false
+            }
+            dateFormatter="string"
+            actionRef={actionRef}
+            toolbar={{
+              actions: [
+                <ModalForm
+                  key="addmodal"
+                  title="添加用户"
+                  trigger={
+                    <Button icon={<PlusOutlined />} type="primary">
+                      新建
+                    </Button>
+                  }
+                  autoFocusFirstInput
+                  modalProps={{
+                    destroyOnHidden: true,
+                  }}
+                  submitTimeout={2000}
+                  onFinish={executeAddUser}
+                >
+                  <ProForm.Group>
+                    <ProFormText
+                      width="md"
+                      name="nickName"
+                      label="用户昵称"
+                      placeholder="请输入用户昵称"
+                      rules={[{ required: true, message: "请输入用户昵称" }]}
+                    />
+
+                    <ProFormTreeSelect
+                      width="md"
+                      name="deptId"
+                      label="归属部门"
+                      placeholder="请选择归属部门"
+                      request={async () => {
+                        return orgSelectData;
+                      }}
+                      fieldProps={{
+                        filterTreeNode: true,
+                        showSearch: true,
+                        treeNodeFilterProp: "name",
+                        fieldNames: {
+                          label: "name",
+                          value: "id",
+                        },
+                      }}
+                    />
+                  </ProForm.Group>
+                  <ProForm.Group>
+                    <ProFormText
+                      width="md"
+                      name="phonenumber"
+                      label="手机号码"
+                      placeholder="请输入手机号码"
+                      rules={[
+                        {
+                          pattern: /^1\d{10}$/,
+                          message: "请输入正确的手机号码",
+                        },
+                      ]}
+                    />
+                    <ProFormText
+                      width="md"
+                      name="email"
+                      label="邮箱"
+                      placeholder="请输入邮箱"
+                      rules={[
+                        { type: "email", message: "请输入正确的邮箱地址" },
+                      ]}
+                    />
+                  </ProForm.Group>
+                  <ProForm.Group>
+                    <ProFormText
+                      width="md"
+                      name="userName"
+                      label="用户名称"
+                      placeholder="请输入用户名称"
+                      rules={[{ required: true, message: "请输入用户名称" }]}
+                    />
+                    <ProFormText.Password
+                      width="md"
+                      name="password"
+                      label="用户密码"
+                      initialValue={defaultPassword}
+                      placeholder="请输入用户密码"
+                    />
+                  </ProForm.Group>
+                  <ProForm.Group>
+                    <ProFormSelect
+                      width="md"
+                      name="sex"
+                      label="用户性别"
+                      request={querySexType}
+                      fieldProps={{
+                        fieldNames: {
+                          label: "dictLabel",
+                          value: "dictValue",
+                        },
+                      }}
+                    />
+                    <ProFormRadio.Group
+                      name="status"
+                      width="sm"
+                      label="状态"
+                      initialValue="0"
+                      options={[
+                        {
+                          label: "正常",
+                          value: "0",
+                        },
+                        {
+                          label: "停用",
+                          value: "1",
+                        },
+                      ]}
+                    />
+                  </ProForm.Group>
+                  <ProForm.Group>
+                    <ProFormSelect
+                      width="md"
+                      name="postIds"
+                      label="岗位"
+                      fieldProps={{
+                        mode: "multiple",
+                      }}
+                      valueEnum={positionValue}
+                    />
+                    <ProFormSelect
+                      width="md"
+                      name="roleIds"
+                      label="角色"
+                      fieldProps={{
+                        mode: "multiple",
+                      }}
+                      valueEnum={roleValue}
+                    />
+                  </ProForm.Group>
+
+                  <ProFormTextArea
+                    name="remark"
+                    width={688}
+                    label="备注"
+                    placeholder="请输入内容"
+                  />
+                </ModalForm>,
+                <ModalForm
+                  key="modifymodal"
+                  title="修改用户"
+                  formRef={modifyFormRef}
+                  trigger={
+                    <Button
+                      icon={<FontAwesomeIcon icon={faPenToSquare} />}
+                      disabled={!rowCanModify}
+                      onClick={() => showRowModifyModal()}
+                    >
+                      修改
+                    </Button>
+                  }
+                  open={showModifyUserModal}
+                  autoFocusFirstInput
+                  modalProps={{
+                    destroyOnHidden: true,
+                    onCancel: () => {
+                      setShowModifyUserModal(false);
+                    },
+                  }}
+                  submitTimeout={2000}
+                  onFinish={executeModifyUser}
+                >
+                  <ProForm.Group>
+                    <ProFormText
+                      width="md"
+                      name="nickName"
+                      label="用户昵称"
+                      placeholder="请输入用户昵称"
+                      rules={[{ required: true, message: "请输入用户昵称" }]}
+                    />
+
+                    <ProFormTreeSelect
+                      width="md"
+                      name="deptId"
+                      label="归属部门"
+                      placeholder="请选择归属部门"
+                      request={async () => {
+                        return orgSelectData;
+                      }}
+                      fieldProps={{
+                        filterTreeNode: true,
+                        showSearch: true,
+                        treeNodeFilterProp: "label",
+                        fieldNames: {
+                          label: "label",
+                          value: "key",
+                        },
+                      }}
+                    />
+                  </ProForm.Group>
+                  <ProForm.Group>
+                    <ProFormText
+                      width="md"
+                      name="phonenumber"
+                      label="手机号码"
+                      placeholder="请输入手机号码"
+                      rules={[
+                        {
+                          pattern: /^1\d{10}$/,
+                          message: "请输入正确的手机号码",
+                        },
+                      ]}
+                    />
+                    <ProFormText
+                      width="md"
+                      name="email"
+                      label="邮箱"
+                      placeholder="请输入邮箱"
+                      rules={[
+                        { type: "email", message: "请输入正确的邮箱地址" },
+                      ]}
+                    />
+                  </ProForm.Group>
+                  <ProForm.Group>
+                    <ProFormSelect
+                      width="md"
+                      name="sex"
+                      label="用户性别"
+                      request={querySexType}
+                      fieldProps={{
+                        fieldNames: {
+                          label: "dictLabel",
+                          value: "dictValue",
+                        },
+                      }}
+                    />
+                    <ProFormRadio.Group
+                      name="status"
+                      width="sm"
+                      label="状态"
+                      options={[
+                        {
+                          label: "正常",
+                          value: "0",
+                        },
+                        {
+                          label: "停用",
+                          value: "1",
+                        },
+                      ]}
+                    />
+                  </ProForm.Group>
+                  <ProForm.Group>
+                    <ProFormSelect
+                      width="md"
+                      name="postIds"
+                      label="岗位"
+                      fieldProps={{
+                        mode: "multiple",
+                      }}
+                      options={modifyPositionValue}
+                    />
+                    <ProFormSelect
+                      width="md"
+                      name="roleIds"
+                      label="角色"
+                      fieldProps={{
+                        mode: "multiple",
+                      }}
+                      options={modifyRoleValue}
+                    />
+                  </ProForm.Group>
+
+                  <ProFormTextArea
+                    name="remark"
+                    width={688}
+                    label="备注"
+                    placeholder="请输入内容"
+                  />
+                </ModalForm>,
+
+                <Button
+                  key="danger"
+                  danger
+                  icon={<DeleteOutlined />}
+                  disabled={!rowCanDelete}
+                  onClick={() => onClickDeleteRow()}
+                >
+                  删除
+                </Button>,
+                <Button
+                  key="import"
+                  type="primary"
+                  icon={<FontAwesomeIcon icon={faUpload} />}
+                  onClick={onClickImport}
+                >
+                  导入
+                </Button>,
+                <Button
+                  key="export"
+                  type="primary"
+                  icon={<FontAwesomeIcon icon={faDownload} />}
+                  onClick={exportTable}
+                >
+                  导出
+                </Button>,
+              ],
+              settings: [
+                {
+                  key: "switch",
+                  icon: showSearch ? (
+                    <FontAwesomeIcon icon={faToggleOn} />
+                  ) : (
+                    <FontAwesomeIcon icon={faToggleOff} />
+                  ),
+                  tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
+                  onClick: (key: string | undefined) => {
+                    setShowSearch(!showSearch);
+                  },
+                },
+                {
+                  key: "refresh",
+                  tooltip: "刷新",
+                  icon: <ReloadOutlined />,
+                  onClick: (key: string | undefined) => {
+                    if (actionRef.current) {
+                      actionRef.current.reload();
+                    }
+                  },
+                },
+              ],
+            }}
+          />
+        </Col>
+      </Row>
+
+      <Modal
+        title={`修改${attachUserdata["userName"]}密码`}
+        open={showModifyUserPwdModal}
+        onOk={confirmModifyUserPwd}
+        onCancel={cancelModifyUserPwd}
+      >
+        <Form form={pwdFormRef} onFinish={executeModifyUserPwd}>
+          <Form.Item
+            label="新密码"
+            name="password"
+            rules={[{ required: true, message: "请输入新密码" }]}
+          >
+            <Input.Password />
+          </Form.Item>
+        </Form>
+      </Modal>
+
+      <Modal
+        title="用户导入"
+        open={showImportModal}
+        onOk={executeImport}
+        onCancel={cancelImportModal}
+      >
+        <Flex justify="center">
+          <div>
+            <Dragger
+              name="file"
+              accept=".xls,.xlsx"
+              listType="text"
+              multiple={false}
+              fileList={fileList}
+              beforeUpload={beforeUpload}
+              onChange={handleChange}
+              onRemove={removeFile}
+              showUploadList={{
+                showDownloadIcon: false,
+                showRemoveIcon: true,
+                removeIcon: <CloseOutlined />,
+              }}
+            >
+              <p className="ant-upload-drag-icon">
+                {uploading ? <LoadingOutlined /> : <FileAddOutlined />}
+              </p>
+              <p className="ant-upload-text">点击此处或拖曳文件到此处上传</p>
+              <p className="ant-upload-hint">仅支持 xls、xlsx 格式文件</p>
+            </Dragger>
+          </div>
+        </Flex>
+        <Flex justify="center" style={{ marginTop: 30 }}>
+          <Typography.Text>
+            <Checkbox
+              checked={uploadSupport}
+              onChange={(e) => {
+                setUploadSupport(e.target.checked);
+              }}
+            >
+              允许更新已有用户的数据
+            </Checkbox>
+          </Typography.Text>
+        </Flex>
+      </Modal>
+    </PageContainer>
+  );
+}

+ 12 - 0
app/(business)/test/test2/page.tsx

@@ -0,0 +1,12 @@
+'use client'
+import React from 'react';
+
+function Page() {
+  return (
+    <div className="text-5xl font-bold underline">
+      Hello world!
+    </div>
+  );
+}
+
+export default Page;

+ 11 - 0
app/(business)/test1/page.tsx

@@ -0,0 +1,11 @@
+import React from 'react';
+
+function Page() {
+  return (
+    <>
+      城市生命线驾驶舱
+    </>
+  );
+}
+
+export default Page;

+ 12 - 0
app/(business)/tool/build/page.tsx

@@ -0,0 +1,12 @@
+"use client";
+
+import { PageContainer } from "@ant-design/pro-components";
+
+export default function Build() {
+
+  return (
+    <PageContainer title={false}>
+      <span>TODO</span>
+    </PageContainer>
+  );
+}

+ 12 - 0
app/(business)/tool/gen/page.tsx

@@ -0,0 +1,12 @@
+"use client";
+
+import { PageContainer } from "@ant-design/pro-components";
+
+export default function Generator() {
+
+  return (
+    <PageContainer title={false}>
+      <span>TODO</span>
+    </PageContainer>
+  );
+}

+ 19 - 0
app/(business)/tool/swagger/page.tsx

@@ -0,0 +1,19 @@
+"use client";
+
+import { PageContainer } from "@ant-design/pro-components";
+
+export default function Swagger() {
+
+  return (
+    <PageContainer title={false}>
+      <div style={{ height: "100vh" }}>
+        <iframe
+          src="/api/swagger-ui/index.html"
+          width="100%"
+          height="100%"
+          style={{border: "none"}}
+        ></iframe>
+      </div>
+    </PageContainer>
+  );
+}

+ 409 - 0
app/(business)/user/profile/page.tsx

@@ -0,0 +1,409 @@
+"use client";
+
+import { UserDetailInfo } from "@/app/_modules/definies";
+import {
+  PageContainer,
+  ProCard,
+  ProDescriptions,
+  ProForm,
+  ProFormRadio,
+  ProFormText,
+} from "@ant-design/pro-components";
+import { Col, Divider, Flex, message, Row, Space, Tabs, Upload } from "antd";
+
+import type { GetProp, TabsProps, UploadProps } from "antd";
+
+import { fetchApi } from "@/app/_modules/func";
+
+import {
+  CalendarOutlined,
+  MailOutlined,
+  PhoneOutlined,
+  UserOutlined,
+} from "@ant-design/icons";
+
+import { faSitemap, faUsers } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useRouter } from "@/node_modules/next/navigation";
+import { useState } from "react";
+
+type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];
+
+const getBase64 = (img: FileType, callback: (url: string) => void) => {
+  const reader = new FileReader();
+  reader.addEventListener("load", () => callback(reader.result as string));
+  reader.readAsDataURL(img);
+};
+
+//上传图片前校验
+const beforeUpload = (file: FileType) => {
+  const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png";
+  if (!isJpgOrPng) {
+    message.error("只能上传 JPG/PNG 格式图片!");
+  }
+  const isLt2M = file.size / 1024 / 1024 < 2;
+  if (!isLt2M) {
+    message.error("图片大小不能超过 2MB!");
+  }
+  return isJpgOrPng && isLt2M;
+};
+
+export default function Profile() {
+  const [imageUrl, setImageUrl] = useState<string>();
+
+  const { push } = useRouter();
+
+  const [user, setUser] = useState({} as UserDetailInfo);
+
+  //获取用户profile
+  const getProfile = async () => {
+    const body = await fetchApi("/api/system/user/profile", push);
+    if (body !== undefined) {
+      const data = body.data;
+      const userData: UserDetailInfo = {
+        userName: data.userName,
+        phonenumber: data.phonenumber,
+        email: data.email,
+        deptName: data.dept.deptName,
+        postGroup: body.postGroup,
+        roleName: data.roles[0].roleName,
+        nickName: data.nickName,
+        sex: data.sex,
+        createTime: data.createTime,
+      };
+
+      setUser(userData);
+      setImageUrl(
+        data.avatar === ""
+          ? userData.sex === "1"
+            ? "/avatar1.jpeg"
+            : "/avatar0.jpeg"
+          : "/api" + data.avatar
+      );
+
+      return userData;
+    }
+  };
+
+  const handleChange: UploadProps["onChange"] = (info) => {
+    if (info.file.status === "uploading") {
+      return;
+    }
+    if (info.file.status === "done") {
+      // Get this url from response in real world.
+      getBase64(info.file.originFileObj as FileType, (url) => {
+        setImageUrl(url);
+      });
+    }
+  };
+
+  //更新用户基本信息
+  const updateProfile = async (values: any) => {
+    const body = await fetchApi("/api/system/user/profile", push, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(values),
+    });
+
+    return body;
+  };
+
+  //修改用户密码
+  const updatePassword = async (values: any) => {
+    const params = {
+      oldPassword: values.oldPassword,
+      newPassword: values.newPassword,
+    };
+    const body = await fetchApi(
+      `/api/system/user/profile/updatePwd?${new URLSearchParams(params)}`,
+      push,
+      {
+        method: "PUT",
+      }
+    );
+
+    return body;
+  };
+
+  const uploadAvatar = async (options: any) => {
+    const formData = new FormData();
+    formData.append("avatarfile", options.file);
+    const body = await fetchApi("/api/system/user/profile/avatar", push, {
+      method: "POST",
+      body: formData,
+    });
+    if (body.code == 200) {
+      message.success("上传头像成功");
+      setImageUrl("/api" + body.imgUrl);
+    } else {
+      message.error(body.msg);
+    }
+  };
+
+  const executeUpdateProfile = async (values: any) => {
+    const body = await updateProfile(values);
+    if (body.code == 200) {
+      message.success("修改成功");
+    } else {
+      message.error(body.msg);
+    }
+  };
+
+  const executeUpdatePassword = async (values: any) => {
+    const body = await updatePassword(values);
+    if (body.code == 200) {
+      message.success("修改成功");
+    } else {
+      message.error(body.msg);
+    }
+  };
+
+  //定义的基本资料的tab页
+  const items: TabsProps["items"] = [
+    {
+      key: "1",
+      label: "基本资料",
+      children: (
+        <ProForm
+          labelCol={{ span: 6 }}
+          wrapperCol={{ span: 14 }}
+          layout="horizontal"
+          submitter={{
+            render: (props, doms) => {
+              return (
+                <Row>
+                  <Col span={14} offset={4}>
+                    <Space>{doms}</Space>
+                  </Col>
+                </Row>
+              );
+            },
+          }}
+          onFinish={executeUpdateProfile}
+          request={getProfile}
+        >
+          <ProFormText
+            width="md"
+            name="nickName"
+            label="用户昵称"
+            placeholder="请输入用户昵称"
+            rules={[{ required: true, message: "请输入用户昵称" }]}
+          />
+          <ProFormText
+            width="md"
+            name="phonenumber"
+            label="手机号"
+            placeholder="请输入手机号"
+            rules={[{ required: true, message: "请输入手机号" }]}
+          />
+          <ProFormText
+            name="email"
+            width="md"
+            label="邮箱"
+            placeholder="请输入邮箱"
+            rules={[{ required: true, message: "请输入邮箱" }]}
+          />
+          <ProFormRadio.Group
+            name="sex"
+            width="md"
+            label="性别"
+            options={[
+              {
+                label: "男",
+                value: "0",
+              },
+              {
+                label: "女",
+                value: "1",
+              },
+            ]}
+            rules={[{ required: true, message: "请选择性别" }]}
+          />
+        </ProForm>
+      ),
+    },
+    {
+      key: "2",
+      label: "修改密码",
+      children: (
+        <ProForm<{
+          oldPassword: string;
+          newPassword: string;
+        }>
+          labelCol={{ span: 6 }}
+          wrapperCol={{ span: 14 }}
+          layout="horizontal"
+          submitter={{
+            render: (props, doms) => {
+              return (
+                <Row>
+                  <Col span={14} offset={4}>
+                    <Space>{doms}</Space>
+                  </Col>
+                </Row>
+              );
+            },
+          }}
+          onFinish={executeUpdatePassword}
+          params={{}}
+        >
+          <ProFormText.Password
+            width="md"
+            name="oldPassword"
+            label="当前密码"
+            placeholder="请输入当前密码"
+            rules={[{ required: true, message: "请输入当前密码" }]}
+          />
+          <ProFormText.Password
+            width="md"
+            name="newPassword"
+            label="新密码"
+            placeholder="请输入新密码"
+            rules={[{ required: true, message: "请输入新密码" }]}
+          />
+          <ProFormText.Password
+            width="md"
+            name="repeatPassword"
+            label="确认新密码"
+            placeholder="请再次输入新密码"
+            rules={[
+              { required: true, message: "请再次输入新密码" },
+              ({ getFieldValue }) => ({
+                validator(_, value) {
+                  if (!value || getFieldValue("newPassword") === value) {
+                    return Promise.resolve();
+                  }
+                  return Promise.reject(new Error("新密码两次输入不一致"));
+                },
+              }),
+            ]}
+          />
+        </ProForm>
+      ),
+    },
+  ];
+
+  return (
+    <PageContainer
+      header={{
+        title: "个人中心",
+        breadcrumb: {},
+      }}
+    >
+      <ProCard gutter={[16, 16]}>
+        <ProCard
+          colSpan="30%"
+          title="个人信息"
+          headerBordered
+          bordered
+          hoverable
+        >
+          <Flex justify="center" align="center">
+            <div>
+              <Upload
+                name="avatarfile"
+                accept=".jpg,.jpeg,.png"
+                listType="picture-circle"
+                className="avatar-uploader"
+                showUploadList={false}
+                customRequest={uploadAvatar}
+                beforeUpload={beforeUpload}
+                onChange={handleChange}
+              >
+                <div
+                  style={{
+                    width: "100px",
+                    height: "100px",
+                    borderRadius: "50%",
+                    overflow: "hidden",
+                  }}
+                >
+                  {imageUrl && (
+                    <img
+                      style={{
+                        width: "100%",
+                        height: "100%",
+                        position: "relative",
+                      }}
+                      src={imageUrl}
+                      alt="avatar"
+                    />
+                  )}
+                </div>
+              </Upload>
+            </div>
+          </Flex>
+          <Divider />
+          <ProDescriptions column={1}>
+            <ProDescriptions.Item
+              label={
+                <>
+                  <UserOutlined />
+                  用户名
+                </>
+              }
+            >
+              {user.userName}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item
+              label={
+                <>
+                  <PhoneOutlined />
+                  手机号码
+                </>
+              }
+            >
+              {user.phonenumber}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item
+              label={
+                <>
+                  <MailOutlined />
+                  用户邮箱
+                </>
+              }
+            >
+              {user.email}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item
+              label={
+                <>
+                  <FontAwesomeIcon icon={faSitemap} />
+                  所属部门
+                </>
+              }
+            >
+              {user.deptName}/{user.postGroup}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item
+              label={
+                <>
+                  <FontAwesomeIcon icon={faUsers} />
+                  所属角色
+                </>
+              }
+            >
+              {user.roleName}
+            </ProDescriptions.Item>
+            <ProDescriptions.Item
+              label={
+                <>
+                  <CalendarOutlined />
+                  创建时间
+                </>
+              }
+            >
+              {user.createTime}
+            </ProDescriptions.Item>
+          </ProDescriptions>
+        </ProCard>
+        <ProCard title="基本资料" headerBordered bordered hoverable>
+          <Tabs defaultActiveKey="1" items={items} />
+        </ProCard>
+      </ProCard>
+    </PageContainer>
+  );
+}

+ 356 - 0
app/_modules/definies.tsx

@@ -0,0 +1,356 @@
+import { ReactNode } from "react";
+
+import { CronLocalization } from "@sbzen/re-cron";
+
+import {
+  ApiOutlined,
+  BookOutlined,
+  ChromeFilled,
+  CodeOutlined,
+  EditOutlined,
+  GithubOutlined,
+  HomeOutlined,
+  LogoutOutlined,
+  MenuOutlined,
+  MessageOutlined,
+  MonitorOutlined,
+  QuestionCircleFilled,
+  SearchOutlined,
+  ToolOutlined,
+  UserOutlined,
+  ExclamationCircleFilled,
+  AreaChartOutlined,
+  PieChartOutlined,
+  BarChartOutlined,
+  LineChartOutlined,
+  SlidersOutlined,
+  PhoneOutlined,
+  AndroidOutlined,
+  AppleOutlined,
+  WindowsOutlined,
+  ChromeOutlined,
+  WechatOutlined,
+  AccountBookOutlined,
+  BankOutlined,
+  BugOutlined,
+  CarOutlined,
+  ClearOutlined,
+  CloudOutlined,
+  EnvironmentOutlined,
+  ExperimentOutlined,
+  FormatPainterOutlined,
+  MailOutlined,
+  ShoppingCartOutlined,
+  SyncOutlined,
+  WifiOutlined,
+} from "@ant-design/icons";
+
+import {
+  faAddressCard,
+  faBookAtlas,
+  faChalkboardUser,
+  faDatabase,
+  faDesktop,
+  faFileWaveform,
+  faGear,
+  faList,
+  faLocationArrow,
+  faMemory,
+  faReceipt,
+  faRectangleList,
+  faSitemap,
+  faTableCells,
+  faThumbtack,
+  faUsers,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+//登录请求
+export type LoginReq = {
+  username: string;
+  password: string;
+  code: string;
+  uuid: string;
+};
+
+//用户简单信息定义
+export type UserInfo = {
+  nickName: string;
+  avatar: string;
+};
+
+//用户详细信息
+export type UserDetailInfo = {
+  userName: string;
+  phonenumber: string;
+  email: string;
+  deptName: string;
+  postGroup: string;
+  roleName: string;
+  nickName: string;
+  sex: number | string;
+  createTime: string;
+};
+
+//路由/菜单定义
+export type RouteInfo = {
+  path: string;
+  name?: string;
+  icon?: ReactNode;
+  routes?: Array<RouteInfo>;
+};
+
+export type IconType = {
+  [key: string]: ReactNode;
+};
+
+//图标映射
+export const IconMap: IconType = {
+  system: <FontAwesomeIcon icon={faGear} />,
+  monitor: <MonitorOutlined />,
+  tool: <ToolOutlined />,
+  guide: <FontAwesomeIcon icon={faLocationArrow} />,
+  user: <UserOutlined />,
+  peoples: <FontAwesomeIcon icon={faUsers} />,
+  treetable: <FontAwesomeIcon icon={faList} />,
+  tree: <FontAwesomeIcon icon={faSitemap} />,
+  post: <FontAwesomeIcon icon={faAddressCard} />,
+  dict: <FontAwesomeIcon icon={faBookAtlas} />,
+  edit: <EditOutlined />,
+  message: <MessageOutlined />,
+  log: <BookOutlined />,
+  online: <FontAwesomeIcon icon={faChalkboardUser} />,
+  job: <FontAwesomeIcon icon={faThumbtack} />,
+  druid: <FontAwesomeIcon icon={faFileWaveform} />,
+  server: <FontAwesomeIcon icon={faDesktop} />,
+  redis: <FontAwesomeIcon icon={faDatabase} />,
+  redislist: <FontAwesomeIcon icon={faMemory} />,
+  build: <FontAwesomeIcon icon={faTableCells} />,
+  code: <CodeOutlined />,
+  swagger: <ApiOutlined />,
+  form: <FontAwesomeIcon icon={faRectangleList} />,
+  logininfor: <FontAwesomeIcon icon={faReceipt} />,
+  areachart: <AreaChartOutlined />,
+  pie: <PieChartOutlined />,
+  barchart: <BarChartOutlined />,
+  linechart: <LineChartOutlined />,
+  slider: <SlidersOutlined />,
+  phone: <PhoneOutlined />,
+  android: <AndroidOutlined />,
+  apple: <AppleOutlined />,
+  window: <WindowsOutlined />,
+  chrome: <ChromeOutlined />,
+  wechat: <WechatOutlined />,
+  account: <AccountBookOutlined />,
+  bank: <BankOutlined />,
+  bug: <BugOutlined />,
+  car: <CarOutlined />,
+  clear: <ClearOutlined />,
+  cloud: <CloudOutlined />,
+  command: <CodeOutlined />,
+  map: <EnvironmentOutlined />,
+  experiment: <ExperimentOutlined />,
+  painter: <FormatPainterOutlined />,
+  home: <HomeOutlined />,
+  mail: <MailOutlined />,
+  shop: <ShoppingCartOutlined />,
+  sync: <SyncOutlined />,
+  wifi: <WifiOutlined />,
+};
+
+//Cron框本地化内容
+export const MortnonCronLocalization: CronLocalization = {
+  common: {
+    month: {
+      january: "一月",
+      february: "二月",
+      march: "三月",
+      april: "四月",
+      may: "五月",
+      june: "六月",
+      july: "七月",
+      august: "八月",
+      september: "九月",
+      october: "十月",
+      november: "十一月",
+      december: "十二月",
+    },
+    dayOfWeek: {
+      sunday: "星期天",
+      monday: "星期一",
+      tuesday: "星期二",
+      wednesday: "星期三",
+      thursday: "星期四",
+      friday: "星期五",
+      saturday: "星期六",
+    },
+    dayOfMonth: {
+      "1st": "第1",
+      "2nd": "第2",
+      "3rd": "第3",
+      "4th": "第4",
+      "5th": "第5",
+      "6th": "第6",
+      "7th": "第7",
+      "8th": "第8",
+      "9th": "第9",
+      "10th": "第10",
+      "11th": "第11",
+      "12th": "第12",
+      "13th": "第13",
+      "14th": "第14",
+      "15th": "第15",
+      "16th": "第16",
+      "17th": "第17",
+      "18th": "第18",
+      "19th": "第19",
+      "20th": "第20",
+      "21st": "第21",
+      "22nd": "第22",
+      "23rd": "第23",
+      "24th": "第24",
+      "25th": "第25",
+      "26th": "第26",
+      "27th": "第27",
+      "28th": "第28",
+      "29th": "第29",
+      "30th": "第30",
+      "31st": "第31",
+    },
+  },
+  tabs: {
+    seconds: "秒",
+    minutes: "分种",
+    hours: "小时",
+    day: "天",
+    month: "月",
+    year: "年",
+  },
+  quartz: {
+    day: {
+      every: {
+        label: "每天",
+      },
+      dayOfWeekIncrement: {
+        label1: "每",
+        label2: "天,从这天开始:",
+      },
+      dayOfMonthIncrement: {
+        label1: "每",
+        label2: "天,从本月",
+        label3: "天开始",
+      },
+      dayOfWeekAnd: {
+        label: "指定每周的某天(选一天或多天)",
+      },
+      dayOfWeekRange: {
+        label1: "每一天,从",
+        label2: "到",
+      },
+      dayOfMonthAnd: {
+        label: "指定每月的某天(选一天或多天)",
+      },
+      dayOfMonthLastDay: {
+        label: "在当月最后一天",
+      },
+      dayOfMonthLastDayWeek: {
+        label: "在当月最后一个工作日",
+      },
+      dayOfWeekLastNTHDayWeek: {
+        label1: "在当月的最后一个",
+        label2: "",
+      },
+      dayOfMonthDaysBeforeEndMonth: {
+        label: "天(在当月结束前)",
+      },
+      dayOfMonthNearestWeekDayOfMonth: {
+        label1: "最接近当月",
+        label2: "天的工作日(星期一至星期五)",
+      },
+      dayOfWeekNTHWeekDayOfMonth: {
+        label1: "在当月",
+        label2: "",
+      },
+    },
+    month: {
+      every: {
+        label: "每月",
+      },
+      increment: {
+        label1: "每",
+        label2: "个月,从这个月开始:",
+      },
+      and: {
+        label: "指定月份(选择一个或多个)",
+      },
+      range: {
+        label1: "每个月,从",
+        label2: "到",
+      },
+    },
+    second: {
+      every: {
+        label: "每秒",
+      },
+      increment: {
+        label1: "每",
+        label2: "秒,从这一秒开始:",
+      },
+      and: {
+        label: "指定秒(选择一个或多个)",
+      },
+      range: {
+        label1: "每秒,从",
+        label2: "到",
+      },
+    },
+    minute: {
+      every: {
+        label: "每分钟",
+      },
+      increment: {
+        label1: "每",
+        label2: "分钟,从这一分钟开始:",
+      },
+      and: {
+        label: "指定分钟(选择一个或多个)",
+      },
+      range: {
+        label1: "每分钟,从",
+        label2: "到",
+      },
+    },
+    hour: {
+      every: {
+        label: "每小时",
+      },
+      increment: {
+        label1: "每",
+        label2: "小时,从这一小时开始:",
+      },
+      and: {
+        label: "指定小时(选择一个或多个)",
+      },
+      range: {
+        label1: "每小时,从",
+        label2: "到",
+      },
+    },
+    year: {
+      every: {
+        label: "任意年份",
+      },
+      increment: {
+        label1: "每",
+        label2: "年,从这一年开始:",
+      },
+      and: {
+        label: "指定年份(选择一个或多个)",
+      },
+      range: {
+        label1: "每年,从",
+        label2: "到",
+      },
+    },
+  }
+};

+ 135 - 0
app/_modules/func.tsx

@@ -0,0 +1,135 @@
+import { getCookie } from "cookies-next";
+import JSEncrypt from "jsencrypt";
+
+export async function fetchApi(url: string, push: any, options?: RequestInit) {
+  const token = getCookie("token");
+  const authHeader = {
+    Authorization: "Bearer " + token,
+  };
+
+  const requestHeader = {
+    ...options?.headers,
+    ...authHeader,
+    credentials: "include",
+  };
+
+  const requestOptions = {
+    ...options,
+    headers: requestHeader,
+  };
+
+  const response = await fetch(url, requestOptions);
+
+  try {
+    const body = await response.json();
+    if (response.ok) {
+      if (body.code == 401) {
+        push("/login");
+        return;
+      }
+    }
+
+    return body;
+  } catch (error) {
+    console.log("fetch error:", error);
+  }
+}
+
+export async function fetchFile(
+  url: string,
+  push: any,
+  options: RequestInit,
+  fileName: string
+) {
+  const token = getCookie("token");
+  const authHeader = {
+    Authorization: "Bearer " + token,
+  };
+
+  const requestHeader = {
+    ...options?.headers,
+    ...authHeader,
+    credentials: "include",
+  };
+
+  const requestOptions = {
+    ...options,
+    headers: requestHeader,
+  };
+
+  const response = await fetch(url, requestOptions);
+
+  try {
+    const contentType = response.headers.get("Content-Type");
+    //如果文件处理出现了json响应信息,肯定是出错了
+    if (contentType && contentType.includes("application/json")) {
+      const body = await response.json();
+      if (response.ok) {
+        if (body.code == 401) {
+          push("/login");
+          return;
+        }
+      }
+    } else {
+      const blob = await response.blob();
+      const url = window.URL.createObjectURL(blob);
+      const a = document.createElement("a");
+      (a.href = url), (a.download = fileName);
+      document.body.appendChild(a);
+      a.click();
+      window.URL.revokeObjectURL(url);
+    }
+  } catch (error) {
+    console.log("fetch file error:", error);
+  }
+}
+
+//公钥
+const publicKey =
+  "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu/JW5RlS6o2Cz3Vlm96u8D+aY3WvzRQyC+B8TQGjOTvtklSXbKYwoccL9RLU1IHUu+Pk1GlZM87OhULq4B3cU9AxXWj24eaxoaWSO4iuMjF7SvsQtWCEmbECJg5WTf8gRi89PjyGntawp04KfR6AXCyTWmTm2vwC+PqoQcnZl9GdNerxeLpD1k7NxSe+syMUaIgM0mVqcirzdPrIVO9v89l5RVxVVUv6XCR3Z05iK8ODQ4ITCoJB8XrGlDPa3TqDlnpME4/upLx3u6oogIRncIPw3P2kAF2viwoll0H23fKkNQvsLdH9jUw+zl0ZJjSb/e2fcMYYrT+AlER3zBOW/wIDAQAB";
+
+//私钥
+const privateKey =
+  "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC78lblGVLqjYLPdWWb3q7wP5pjda/NFDIL4HxNAaM5O+2SVJdspjChxwv1EtTUgdS74+TUaVkzzs6FQurgHdxT0DFdaPbh5rGhpZI7iK4yMXtK+xC1YISZsQImDlZN/yBGLz0+PIae1rCnTgp9HoBcLJNaZOba/AL4+qhBydmX0Z016vF4ukPWTs3FJ76zIxRoiAzSZWpyKvN0+shU72/z2XlFXFVVS/pcJHdnTmIrw4NDghMKgkHxesaUM9rdOoOWekwTj+6kvHe7qiiAhGdwg/Dc/aQAXa+LCiWXQfbd8qQ1C+wt0f2NTD7OXRkmNJv97Z9wxhitP4CURHfME5b/AgMBAAECggEACOp9Ls8dvNzLuNXD5ToSKHmL9G3v0hXELgYPP4P1X1C1e3yh1lin8/TCX3TuPcqO8f7kqyL4RVnpOC8tf0ZLXnqA7QJ+u8a65IU7Q7G/OchZJfx1FXWntLbN+Eoz0+1ndYzmJd6vMDfVF4q/OqJIypaewuoIfZj49yDE/KH7vZTyQQi8dmP2Re86WjlJyt+FrWUpzQh7R/NcDaRJ1fUBpOB+PZ+WYFK4BHTmvZ6f/XDYSbuiIMEcA/BejklaIFIAv9mMZ7MBCs+pPvgtleIlvfZwmCEEwYWCnZPncYovtaeKmTY7KMjp5LbyAeK/mqblqhiXl9mp3NM1Tk6doCKbyQKBgQDkhLlG50t6BO/xl7kkDUkG5QRe6MctoiN5BB/tNlLWImOhzNka+JPu278R2n6Ls4fYD74RxKwcy8a0683TsVC1mo8S/ck6o6HlDmBB+N6IMbMF8snKksKM6yytvOj4O3V8KZdaJDGzTz/CUEIWKUfBCQmbl7iFhOwpo7bNzxA0QwKBgQDSjI2Ew0v8TUDdNfFcd3exDQm3HmA1XcTIAapmVsgcLK76IIDsJSFqA+b8cHTM1I01kiK1kadPy9ZIR1NNb5ZjCNJqzIpzTI2ZdZdBgyQh7tjw9hdzcd2n7aKXmahZ9vNgzGVOhCT4A4RmBvozCuHLpEHT431A8L04WTdLEalklQKBgA8Fenhis99te6hR5OWtyeMeIs9qVc12HwbRcpfRPli9Ifd807imJnNJFqJBzpe4UXGudzwLxZSPAJzb80e7HCcT5dvFuviT0QyRiVpM1bP2MGJvtzwNsaQ5wVIaXOYUYoCq6zwNrQawauyHAhEa3ZCe23bS3lpIho2mKVoWBmapAoGAbepMMuPVdjhKRXFUuEXx6S76RGuKJDH4ecVM1LI3M2YsTo3LX/weTn8NBfobL5dCxJWuowUPyDuMeR0rIsC/TKIdXv26xWhQf62AsgWpRkGvZVPDeFQYOAN5nxTra1PdSEpMFMotloAXjT/VO/JRYAM3DkuzZsSGs7T3hawJt2UCgYEAmVE0x7c1zxD62h68v2jmLdPbG5UVmuSSwVod2PqSv5WDY/7tg1HUh4/UcSX/9rbXXzacMTBE/aJDZkusFSThsmMFkpKmt5a2JOtsbzwFHo0kSCOngUBwxyPY+JEMgDJo9Y+6fnYNnhQklrjiyAdaUGb4YrPQSNas1G29OzE5Mcs=";
+
+//加密数据
+export function encrypt(content: string) {
+  const encryptor = new JSEncrypt();
+  encryptor.setPublicKey(publicKey);
+  return encryptor.encrypt(content);
+}
+
+//解密数据
+export function decrypt(content: string) {
+  const encryptor = new JSEncrypt();
+  encryptor.setPrivateKey(privateKey);
+  return encryptor.decrypt(content);
+}
+
+//获取当前浏览器是否深色模式
+export function displayModeIsDark() {
+  if (typeof window !== 'undefined') {
+    return window.matchMedia("(prefers-color-scheme: dark)").matches;
+  }
+
+  return false;
+}
+
+//动态获取浏览器是否深色模式
+export function watchDarkModeChange(callback: (value: boolean) => void) {
+  if (typeof window !== 'undefined') {
+    const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");
+
+    const onChange = (e: any) => {
+      callback(e.matches);
+    };
+
+    darkModeQuery.addEventListener("change", onChange);
+
+    return () => {
+      darkModeQuery.removeEventListener("change", onChange);
+    };
+  }
+
+  return () => {};
+}

BIN
app/favicon.ico


+ 1 - 0
app/globals.css

@@ -0,0 +1 @@
+@import "tailwindcss";

+ 27 - 0
app/layout.tsx

@@ -0,0 +1,27 @@
+import type { Metadata } from "next";
+import { AntdRegistry } from "@ant-design/nextjs-registry";
+import "./normalize.css";
+import "./globals.css";
+import "@fortawesome/fontawesome-svg-core/styles.css"; // import Font Awesome CSS
+import { config } from "@fortawesome/fontawesome-svg-core";
+config.autoAddCss = false; // Tell Font Awesome to skip adding the CSS automatically since it's being imported above
+
+
+export const metadata: Metadata = {
+  title: "MorTnon RuoYi",
+  description: "MorTnon 版本高级 RuoYi 前台",
+};
+
+export default function RootLayout({
+  children,
+}: Readonly<{
+  children: React.ReactNode;
+}>) {
+  return (
+    <html lang="en">
+      <body>
+          <AntdRegistry>{children}</AntdRegistry>
+      </body>
+    </html>
+  );
+}

+ 339 - 0
app/login/page.tsx

@@ -0,0 +1,339 @@
+"use client";
+import { LockOutlined, UserOutlined } from "@ant-design/icons";
+import {
+  LoginFormPage,
+  ProConfigProvider,
+  ProFormCheckbox,
+  ProFormText,
+} from "@ant-design/pro-components";
+import { Divider, message, Spin, theme, ConfigProvider } from "antd";
+import type { ConfigProviderProps } from "antd";
+import { setCookie, getCookie, deleteCookie } from "cookies-next";
+import { useRouter } from "next/navigation";
+
+import type { ProFormInstance } from "@ant-design/pro-components";
+
+import Image from "next/image";
+
+import { useEffect, useState, useRef } from "react";
+import { LoginReq } from "../_modules/definies";
+import {
+  encrypt,
+  decrypt,
+  displayModeIsDark,
+  watchDarkModeChange,
+} from "../_modules/func";
+
+type Captcha = {
+  img: string;
+  uuid: string;
+};
+
+//cookies 记住的用户名 key
+const cookie_username_key = "mortnon_username";
+//cookies 记住的密码 key
+const cookie_password_key = "mortnon_password";
+
+//浅色背景图
+const backgroudLight = "/bg3.jpg";
+//深色前景图
+const backgroundDark = "/bg-dark.jpg";
+
+export default function Login() {
+  //验证码数据
+  const [captcha, setCaptcha] = useState({} as Captcha);
+  //是否展示验证码框
+  const [showCaptcha, setShowCaptcha] = useState(false);
+  //验证码加载状态
+  const [isLoadingImg, setIsLoadingImg] = useState(true);
+
+  //获取验证码
+  const getCaptcha = async () => {
+    try {
+      const response = await fetch("/api/captchaImage");
+      if (response.ok) {
+        const data = await response.json();
+
+        setShowCaptcha(data.captchaEnabled);
+
+        if (data.captchaEnabled) {
+          const imagePrefix = "data:image/gif;base64,";
+
+          const captchaData: Captcha = {
+            img: imagePrefix + data.img,
+            uuid: data.uuid,
+          };
+
+          setCaptcha(captchaData);
+          setIsLoadingImg(false);
+        }
+      } else {
+      }
+    } catch (error) {
+    } finally {
+    }
+  };
+
+  //深色模式
+  const [isDark, setIsDark] = useState(false);
+  //背景图片
+  const [background, setBackground] = useState(backgroudLight);
+
+  useEffect(() => {
+    getCaptcha();
+    readUserNamePassword();
+    setIsDark(displayModeIsDark());
+    setBackground(displayModeIsDark() ? backgroundDark : backgroudLight);
+    const unsubscribe = watchDarkModeChange((matches: boolean) => {
+      setIsDark(matches);
+      setBackground(matches ? backgroundDark : backgroudLight);
+    });
+    return () => {
+      unsubscribe();
+    };
+  }, []);
+
+  const router = useRouter();
+
+  //提交登录
+  const userLogin = async (values: any) => {
+    const loginData: LoginReq = {
+      username: values.username,
+      password: values.password,
+      code: values.code,
+      uuid: captcha.uuid,
+    };
+
+    //是否记住密码
+    const autoLogin = values.autoLogin;
+
+    try {
+      const response = await fetch("/api/login", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(loginData),
+        credentials: "include",
+      });
+
+      //获得响应
+      if (response.ok) {
+        const data = await response.json();
+
+        //登录成功
+        if (data.code == 200) {
+          message.success("登录成功");
+
+          setCookie("token", data.token);
+
+          //记住密码
+          if (autoLogin) {
+            rememberUserNamePassword(values.username, values.password);
+          } else {
+            removeUserNamePassword();
+          }
+
+          router.push("/");
+        } else {
+          message.open({
+            type: "error",
+            content: data.msg,
+          });
+
+          //异常,自动刷新验证码
+          getCaptcha();
+        }
+      } else {
+        const data = await response.json();
+
+        message.open({
+          type: "error",
+          content: data.msg,
+        });
+      }
+    } catch (error) {
+      console.log("error:", error);
+      message.open({
+        type: "error",
+        content: "登录发生异常,请重试",
+      });
+    } finally {
+    }
+  };
+
+  //记住用户名密码到cookie
+  const rememberUserNamePassword = (username: string, password: string) => {
+    setCookie(cookie_username_key, encrypt(username));
+    setCookie(cookie_password_key, encrypt(password));
+  };
+
+  //移除cookie中的用户名和密码
+  const removeUserNamePassword = () => {
+    deleteCookie(cookie_username_key);
+    deleteCookie(cookie_password_key);
+  };
+
+  const loginFormRef = useRef<ProFormInstance>();
+
+  //读取cookie中用户名密码,并填写到表单中
+  const readUserNamePassword = () => {
+    const username = getCookie(cookie_username_key);
+    const password = getCookie(cookie_password_key);
+
+    if (username !== undefined && password !== undefined) {
+      if (loginFormRef) {
+        loginFormRef.current?.setFieldsValue({
+          username: decrypt(username),
+          password: decrypt(password),
+          autoLogin: true,
+        });
+      }
+    }
+  };
+
+  const { token } = theme.useToken();
+
+  return (
+    <ProConfigProvider dark={isDark}>
+      <div
+        style={{
+          backgroundColor: "white",
+          height: "100vh",
+        }}
+      >
+        <LoginFormPage
+          formRef={loginFormRef}
+          backgroundImageUrl={background}
+          logo="https://static.dongfangzan.cn/img/mortnon.svg"
+          title={(<span>MorTnon 若依后台管理</span>) as any}
+          containerStyle={{
+            backgroundColor: "rgba(0,0,0,0)",
+            backdropFilter: "blur(4px)",
+          }}
+          subTitle={
+            <span style={{ color: "rgba(255,255,255,1)" }}>
+              MorTnon,高质量的快速开发框架
+            </span>
+          }
+          actions={
+            <div
+              style={{
+                display: "flex",
+                justifyContent: "center",
+                alignItems: "center",
+              }}
+            >
+              <p style={{ color: "rgba(255,255,255,.6)" }}>
+                ©{new Date().getFullYear()} Mortnon.
+              </p>
+            </div>
+          }
+          onFinish={userLogin}
+        >
+          <Divider>账号密码登录</Divider>
+          <>
+            <ProFormText
+              name="username"
+              fieldProps={{
+                size: "large",
+                prefix: (
+                  <UserOutlined
+                    style={{
+                      color: token.colorText,
+                    }}
+                    className={"prefixIcon"}
+                  />
+                ),
+              }}
+              placeholder={"用户名"}
+              rules={[
+                {
+                  required: true,
+                  message: "用户名不能为空",
+                },
+              ]}
+            />
+            <ProFormText.Password
+              name="password"
+              fieldProps={{
+                size: "large",
+                prefix: (
+                  <LockOutlined
+                    style={{
+                      color: token.colorText,
+                    }}
+                    className={"prefixIcon"}
+                  />
+                ),
+              }}
+              placeholder={"密码"}
+              rules={[
+                {
+                  required: true,
+                  message: "密码不能为空",
+                },
+              ]}
+            />
+            {showCaptcha && (
+              <div
+                style={{
+                  display: "flex",
+                  justifyContent: "center",
+                  flexDirection: "row",
+                }}
+              >
+                <ProFormText
+                  name="code"
+                  fieldProps={{
+                    size: "large",
+                    prefix: (
+                      <UserOutlined
+                        style={{
+                          color: token.colorText,
+                        }}
+                        className={"prefixIcon"}
+                      />
+                    ),
+                  }}
+                  placeholder={"验证码"}
+                  rules={[
+                    {
+                      required: true,
+                      message: "验证码不能为空",
+                    },
+                  ]}
+                />
+
+                <div style={{ margin: "0 0 0 8px" }}>
+                  <Spin spinning={isLoadingImg}>
+                    {captcha.img === undefined ? (
+                      <div style={{ width: 80, height: 40 }}></div>
+                    ) : (
+                      <Image
+                        src={captcha.img}
+                        width={80}
+                        height={40}
+                        alt="captcha"
+                        onClick={getCaptcha}
+                      />
+                    )}
+                  </Spin>
+                </div>
+              </div>
+            )}
+          </>
+          <div
+            style={{
+              marginBlockEnd: 24,
+            }}
+          >
+            <ProFormCheckbox noStyle name="autoLogin">
+              记住密码
+            </ProFormCheckbox>
+          </div>
+        </LoginFormPage>
+      </div>
+    </ProConfigProvider>
+  );
+}

+ 348 - 0
app/normalize.css

@@ -0,0 +1,348 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+   ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+html {
+  line-height: 1.15; /* 1 */
+  -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+   ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+  margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+  display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content
+   ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+  box-sizing: content-box; /* 1 */
+  height: 0; /* 1 */
+  overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+   ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+  background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+  border-bottom: none; /* 1 */
+  text-decoration: underline; /* 2 */
+  text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content
+   ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+  border-style: none;
+}
+
+/* Forms
+   ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: inherit; /* 1 */
+  font-size: 100%; /* 1 */
+  line-height: 1.15; /* 1 */
+  margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+  overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+  text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+  padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ *    `fieldset` elements in all browsers.
+ */
+
+legend {
+  box-sizing: border-box; /* 1 */
+  color: inherit; /* 2 */
+  display: table; /* 1 */
+  max-width: 100%; /* 1 */
+  padding: 0; /* 3 */
+  white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+  vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+  overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/* Interactive
+   ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+  display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+  display: list-item;
+}
+
+/* Misc
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+  display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+  display: none;
+}

+ 3 - 0
app/page.module.css

@@ -0,0 +1,3 @@
+.bodyContent {
+  height: 100vh;
+}

+ 57 - 0
app/page.tsx

@@ -0,0 +1,57 @@
+"use client";
+
+import { Flex, Spin } from "antd";
+
+import { getCookie } from "cookies-next";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+import PuffLoader from "react-spinners/PuffLoader";
+
+import styles from "./page.module.css";
+import { displayModeIsDark } from "./_modules/func";
+
+//浅色
+const lightColor = "white";
+//深色
+const darkColor = "black";
+//主要颜色
+const color = "#1677ff";
+
+export default function Home() {
+  const { push } = useRouter();
+
+  //是否深色模式
+  const [backgroundColor, setBackgroundColor] = useState(lightColor);
+
+  useEffect(() => {
+    const token = getCookie("token");
+    if (token === "") {
+      push("/login");
+    } else {
+      push("/home");
+    }
+
+    setBackgroundColor(displayModeIsDark() ? darkColor : lightColor);
+  }, []);
+
+  return (
+    <Flex
+      vertical
+      className={styles.bodyContent}
+      style={{ backgroundColor: backgroundColor }}
+      justify="center"
+      align="center"
+    >
+      <PuffLoader
+        color={color}
+        loading={true}
+        size={150}
+        aria-label="Loading"
+      />
+      <span style={{ color: color, marginTop: "16px" }}>
+        MorTnon,高质量的快速开发框架
+      </span>
+    </Flex>
+  );
+}

+ 5 - 0
eslint.config.mjs

@@ -11,6 +11,11 @@ const compat = new FlatCompat({
 
 const eslintConfig = [
   ...compat.extends("next/core-web-vitals", "next/typescript"),
+  {
+    rules: {
+      "@typescript-eslint/no-explicit-any": "off"
+    }
+  }
 ];
 
 export default eslintConfig;

+ 30 - 4
next.config.ts

@@ -1,7 +1,33 @@
-import type { NextConfig } from "next";
+/** @type {import('next').NextConfig} */
 
-const nextConfig: NextConfig = {
-  /* config options here */
+const nextConfig = {
+  //代理重定向到后台服务
+  async rewrites() {
+    return {
+      fallback: [
+        {
+          source: "/api/system/user",
+          destination: `${process.env.BACKEND_URL}/system/user/`,
+        },
+        {
+          source: "/api/:path*",
+          destination: `${process.env.BACKEND_URL}/:path*`,
+        },
+      ],
+    };
+  },
+  images: {
+    dangerouslyAllowSVG: true,
+    contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
+    remotePatterns: [
+      {
+        protocol: "https",
+        hostname: "img.shields.io",
+        port: "",
+        pathname: "/**",
+      },
+    ],
+  },
 };
 
-export default nextConfig;
+module.exports = nextConfig;

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1905 - 1248
package-lock.json


+ 23 - 6
package.json

@@ -9,19 +9,36 @@
     "lint": "next lint"
   },
   "dependencies": {
+    "@ant-design/nextjs-registry": "^1.1.0",
+    "@ant-design/pro-components": "^2.8.10",
+    "@emotion/css": "^11.13.5",
+    "@fortawesome/fontawesome-svg-core": "^7.0.0",
+    "@fortawesome/free-solid-svg-icons": "^7.0.0",
+    "@fortawesome/react-fontawesome": "^0.2.3",
+    "@sbzen/re-cron": "^2.0.7",
+    "antd": "^5.26.7",
+    "chart.js": "^4.5.0",
+    "cookies-next": "^6.1.0",
+    "jsencrypt": "^3.3.2",
+    "next": "15.4.6",
     "react": "19.1.0",
+    "react-chartjs-2": "^5.3.0",
+    "react-countup": "^6.5.3",
+    "react-d3-speedometer": "^3.1.1",
     "react-dom": "19.1.0",
-    "next": "15.4.6"
+    "react-quill": "^2.0.0",
+    "react-spinners": "^0.17.0",
+    "sass": "^1.90.0"
   },
   "devDependencies": {
-    "typescript": "^5",
+    "@eslint/eslintrc": "^3",
+    "@tailwindcss/postcss": "^4",
     "@types/node": "^20",
     "@types/react": "^19",
     "@types/react-dom": "^19",
-    "@tailwindcss/postcss": "^4",
-    "tailwindcss": "^4",
     "eslint": "^9",
-    "eslint-config-next": "15.4.6",
-    "@eslint/eslintrc": "^3"
+    "eslint-config-next": "^15.4.6",
+    "tailwindcss": "^4",
+    "typescript": "^5"
   }
 }

BIN
public/1.png


BIN
public/2.png


BIN
public/3.png


BIN
public/4.png


BIN
public/5.png


BIN
public/6.png


BIN
public/7.png


+ 43 - 0
public/antd.svg

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="200px" height="200px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch -->
+    <title>Group 28 Copy 5</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <linearGradient x1="62.1023273%" y1="0%" x2="108.19718%" y2="37.8635764%" id="linearGradient-1">
+            <stop stop-color="#4285EB" offset="0%"></stop>
+            <stop stop-color="#2EC7FF" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="69.644116%" y1="0%" x2="54.0428975%" y2="108.456714%" id="linearGradient-2">
+            <stop stop-color="#29CDFF" offset="0%"></stop>
+            <stop stop-color="#148EFF" offset="37.8600687%"></stop>
+            <stop stop-color="#0A60FF" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="69.6908165%" y1="-12.9743587%" x2="16.7228981%" y2="117.391248%" id="linearGradient-3">
+            <stop stop-color="#FA816E" offset="0%"></stop>
+            <stop stop-color="#F74A5C" offset="41.472606%"></stop>
+            <stop stop-color="#F51D2C" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="68.1279872%" y1="-35.6905737%" x2="30.4400914%" y2="114.942679%" id="linearGradient-4">
+            <stop stop-color="#FA8E7D" offset="0%"></stop>
+            <stop stop-color="#F74A5C" offset="51.2635191%"></stop>
+            <stop stop-color="#F51D2C" offset="100%"></stop>
+        </linearGradient>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="logo" transform="translate(-20.000000, -20.000000)">
+            <g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)">
+                <g id="Group-27-Copy-3">
+                    <g id="Group-25" fill-rule="nonzero">
+                        <g id="2">
+                            <path d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z" id="Shape" fill="url(#linearGradient-1)"></path>
+                            <path d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z" id="Shape" fill="url(#linearGradient-2)"></path>
+                        </g>
+                        <path d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z" id="Shape" fill="url(#linearGradient-3)"></path>
+                    </g>
+                    <ellipse id="Combined-Shape" fill="url(#linearGradient-4)" cx="100.519339" cy="100.436681" rx="23.6001926" ry="23.580786"></ellipse>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 58 - 0
public/antdpro.svg

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="380px" height="380px" viewBox="0 0 380 380" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>Logo_Tech UI</title>
+    <defs>
+        <path d="M254.102941,3.81125902 C258.753019,6.29814839 262.402428,9.94755745 264.889318,14.5976363 C267.376207,19.2477151 268.700577,23.8601327 268.700577,36.5796867 L268.700577,232.12089 C268.700577,244.840444 267.376207,249.452862 264.889318,254.102941 C262.402428,258.753019 258.753019,262.402428 254.102941,264.889318 C249.452862,267.376207 244.840444,268.700577 232.12089,268.700577 L36.5796867,268.700577 C23.8601327,268.700577 19.2477151,267.376207 14.5976363,264.889318 C9.94755745,262.402428 6.29814839,258.753019 3.81125902,254.102941 C1.3626295,249.524401 0.0410249883,244.982374 0.000939791477,232.70222 L1.93547515e-12,36.5796867 C1.93104999e-12,23.8601327 1.32436966,19.2477151 3.81125902,14.5976363 C6.29814839,9.94755745 9.94755745,6.29814839 14.5976363,3.81125902 C19.1761754,1.3626295 23.7182032,0.0410249883 35.9983572,0.000939791474 L232.12089,-6.80643163e-13 C244.840444,-6.82979707e-13 249.452862,1.32436966 254.102941,3.81125902 Z M192.608378,76.0921988 L76.0921988,76.0921988 L76.0921988,192.608378 L192.608378,192.608378 L192.608378,76.0921988 Z" id="path-1"></path>
+        <linearGradient x1="50%" y1="6.95213193%" x2="50%" y2="100%" id="linearGradient-3">
+            <stop stop-color="#008CFF" offset="0%"></stop>
+            <stop stop-color="#0063FF" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="7.52933084%" x2="50%" y2="90.9153735%" id="linearGradient-4">
+            <stop stop-color="#04A6FF" offset="0%"></stop>
+            <stop stop-color="#1CC6FF" offset="100%"></stop>
+        </linearGradient>
+        <radialGradient cx="90.4025396%" cy="31.7002625%" fx="90.4025396%" fy="31.7002625%" r="149.547625%" gradientTransform="translate(0.904025,0.317003),scale(0.283186,1.000000),rotate(134.788412),translate(-0.904025,-0.317003)" id="radialGradient-5">
+            <stop stop-color="#51D6FF" offset="0%"></stop>
+            <stop stop-color="#04A6FF" offset="100%"></stop>
+        </radialGradient>
+        <linearGradient x1="0%" y1="0%" x2="83.4936414%" y2="87.8723086%" id="linearGradient-6">
+            <stop stop-color="#FF8384" offset="0%"></stop>
+            <stop stop-color="#FF4553" offset="100%"></stop>
+        </linearGradient>
+        <rect id="path-7" x="76.0282054" y="76.0282054" width="116.516179" height="116.516179"></rect>
+        <filter x="-7.7%" y="-7.7%" width="115.4%" height="115.4%" filterUnits="objectBoundingBox" id="filter-9">
+            <feGaussianBlur stdDeviation="6.5" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="5" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0.423529412   0 0 0 0 1  0 0 0 0.2 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Logo_图形_for-Hitu" transform="translate(-10.000000, -10.000000)">
+            <g id="Logo_Tech-UI" transform="translate(10.000000, 10.000000)">
+                <g id="Draft1" transform="translate(190.000000, 190.000000) rotate(-45.000000) translate(-190.000000, -190.000000) translate(55.649712, 55.649712)">
+                    <mask id="mask-2" fill="white">
+                        <use xlink:href="#path-1"></use>
+                    </mask>
+                    <use id="形状结合" fill="#D8D8D8" xlink:href="#path-1"></use>
+                    <g id="编组" mask="url(#mask-2)">
+                        <g transform="translate(-0.000000, 0.000000)">
+                            <rect id="矩形备份" fill="#0E89FF" transform="translate(131.377937, 230.654477) rotate(-360.000000) translate(-131.377937, -230.654477) " x="70.1474957" y="192.608378" width="122.460882" height="76.0921988"></rect>
+                            <path d="M1.89233657e-11,76.0921988 L76.0921988,76.0921988 L76.0921988,268.700577 L21.5547871,268.700577 C14.059718,268.700577 11.3418249,267.920185 8.60173966,266.454771 C5.86165444,264.989357 3.71121953,262.838922 2.24580592,260.098837 C0.780392306,257.358752 1.89215954e-11,254.640859 1.89264125e-11,247.14579 L1.89233657e-11,76.0921988 L1.89233657e-11,76.0921988 Z" id="矩形" fill="url(#linearGradient-3)" transform="translate(38.046099, 172.396388) rotate(-360.000000) translate(-38.046099, -172.396388) "></path>
+                            <rect id="矩形备份-5" fill="url(#linearGradient-4)" transform="translate(230.654477, 172.396388) rotate(-360.000000) translate(-230.654477, -172.396388) " x="192.608378" y="76.0921988" width="76.0921988" height="192.608378"></rect>
+                            <path d="M21.5547871,5.71774808e-13 L247.14579,5.90358579e-13 C254.640859,5.65221385e-13 257.358752,0.780392306 260.098837,2.24580592 C262.838922,3.71121953 264.989357,5.86165444 266.454771,8.60173966 C267.920185,11.3418249 268.700577,14.059718 268.700577,21.5547871 L268.700577,76.0921988 L268.700577,76.0921988 L-1.89233657e-12,76.0921988 L-1.89356968e-12,21.5547871 C-1.8916201e-12,14.059718 0.780392306,11.3418249 2.24580592,8.60173966 C3.71121953,5.86165444 5.86165444,3.71121953 8.60173966,2.24580592 C11.3418249,0.780392306 14.059718,5.76019091e-13 21.5547871,5.71774808e-13 Z" id="矩形备份-2" fill="#04A6FF" transform="translate(134.350288, 38.046099) rotate(-360.000000) translate(-134.350288, -38.046099) "></path>
+                            <path d="M21.5547871,5.37983083e-13 L247.14579,5.56566855e-13 C254.640859,5.3142966e-13 257.358752,0.780392306 260.098837,2.24580592 C262.838922,3.71121953 264.989357,5.86165444 266.454771,8.60173966 C267.920185,11.3418249 268.700577,14.059718 268.700577,21.5547871 L268.700577,76.0921988 L268.700577,76.0921988 L-1.62200277e-12,76.0921988 L-1.62323588e-12,21.5547871 C-1.6212863e-12,14.059718 0.780392306,11.3418249 2.24580592,8.60173966 C3.71121953,5.86165444 5.86165444,3.71121953 8.60173966,2.24580592 C11.3418249,0.780392306 14.059718,5.42227367e-13 21.5547871,5.37983083e-13 Z" id="矩形备份-2" fill="url(#radialGradient-5)" opacity="0.8" transform="translate(134.350288, 38.046099) rotate(-360.000000) translate(-134.350288, -38.046099) "></path>
+                        </g>
+                    </g>
+                    <path d="M192.608378,192.608378 L268.700577,192.608378 L268.700577,247.14579 C268.700577,254.640859 267.920185,257.358752 266.454771,260.098837 C264.989357,262.838922 262.838922,264.989357 260.098837,266.454771 C257.358752,267.920185 254.640859,268.700577 247.14579,268.700577 L192.608378,268.700577 L192.608378,268.700577 L192.608378,192.608378 Z" id="矩形备份-4" fill="url(#linearGradient-6)" mask="url(#mask-2)" transform="translate(230.654477, 230.654477) rotate(-360.000000) translate(-230.654477, -230.654477) "></path>
+                    <mask id="mask-8" fill="white">
+                        <use xlink:href="#path-7"></use>
+                    </mask>
+                    <g id="shadow" fill="black" fill-opacity="1">
+                        <use filter="url(#filter-9)" xlink:href="#path-7"></use>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
public/avatar0.jpeg


BIN
public/avatar1.jpeg


BIN
public/bg-dark.jpg


BIN
public/bg.jpg


BIN
public/bg2.jpg


BIN
public/bg3.jpg


BIN
public/bg4.jpg


BIN
public/fontawesome.ico


BIN
public/nextjs.ico


BIN
src/app/favicon.ico


+ 0 - 26
src/app/globals.css

@@ -1,26 +0,0 @@
-@import "tailwindcss";
-
-:root {
-  --background: #ffffff;
-  --foreground: #171717;
-}
-
-@theme inline {
-  --color-background: var(--background);
-  --color-foreground: var(--foreground);
-  --font-sans: var(--font-geist-sans);
-  --font-mono: var(--font-geist-mono);
-}
-
-@media (prefers-color-scheme: dark) {
-  :root {
-    --background: #0a0a0a;
-    --foreground: #ededed;
-  }
-}
-
-body {
-  background: var(--background);
-  color: var(--foreground);
-  font-family: Arial, Helvetica, sans-serif;
-}

+ 0 - 34
src/app/layout.tsx

@@ -1,34 +0,0 @@
-import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
-import "./globals.css";
-
-const geistSans = Geist({
-  variable: "--font-geist-sans",
-  subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
-  variable: "--font-geist-mono",
-  subsets: ["latin"],
-});
-
-export const metadata: Metadata = {
-  title: "Create Next App",
-  description: "Generated by create next app",
-};
-
-export default function RootLayout({
-  children,
-}: Readonly<{
-  children: React.ReactNode;
-}>) {
-  return (
-    <html lang="en">
-      <body
-        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
-      >
-        {children}
-      </body>
-    </html>
-  );
-}

+ 0 - 103
src/app/page.tsx

@@ -1,103 +0,0 @@
-import Image from "next/image";
-
-export default function Home() {
-  return (
-    <div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
-      <main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
-        <Image
-          className="dark:invert"
-          src="/next.svg"
-          alt="Next.js logo"
-          width={180}
-          height={38}
-          priority
-        />
-        <ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
-          <li className="mb-2 tracking-[-.01em]">
-            Get started by editing{" "}
-            <code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
-              src/app/page.tsx
-            </code>
-            .
-          </li>
-          <li className="tracking-[-.01em]">
-            Save and see your changes instantly.
-          </li>
-        </ol>
-
-        <div className="flex gap-4 items-center flex-col sm:flex-row">
-          <a
-            className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
-            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-            target="_blank"
-            rel="noopener noreferrer"
-          >
-            <Image
-              className="dark:invert"
-              src="/vercel.svg"
-              alt="Vercel logomark"
-              width={20}
-              height={20}
-            />
-            Deploy now
-          </a>
-          <a
-            className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
-            href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-            target="_blank"
-            rel="noopener noreferrer"
-          >
-            Read our docs
-          </a>
-        </div>
-      </main>
-      <footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
-        <a
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
-          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          <Image
-            aria-hidden
-            src="/file.svg"
-            alt="File icon"
-            width={16}
-            height={16}
-          />
-          Learn
-        </a>
-        <a
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
-          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          <Image
-            aria-hidden
-            src="/window.svg"
-            alt="Window icon"
-            width={16}
-            height={16}
-          />
-          Examples
-        </a>
-        <a
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
-          href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          <Image
-            aria-hidden
-            src="/globe.svg"
-            alt="Globe icon"
-            width={16}
-            height={16}
-          />
-          Go to nextjs.org →
-        </a>
-      </footer>
-    </div>
-  );
-}

+ 1 - 1
tsconfig.json

@@ -19,7 +19,7 @@
       }
     ],
     "paths": {
-      "@/*": ["./src/*"]
+      "@/*": ["./*"]
     }
   },
   "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است