فهرست منبع

feat[backend-cli]: role

chenlei 4 ماه پیش
والد
کامیت
b82831e7dc
23فایلهای تغییر یافته به همراه1486 افزوده شده و 498 حذف شده
  1. 8 2
      packages/backend-cli/CHANGELOG.md
  2. 76 32
      packages/backend-cli/create-dage-backend-cli
  3. 1 1
      packages/backend-cli/package.json
  4. 5 0
      packages/backend-cli/template/.eslintrc.js
  5. 7 2
      packages/backend-cli/template/package.json
  6. 44 23
      packages/backend-cli/template/src/api/user.ts
  7. 129 0
      packages/backend-cli/template/src/pages/Layout/_withoutRole.tsx
  8. 2 1
      packages/backend-cli/template/src/pages/Layout/components/Header/index.tsx
  9. 190 129
      packages/backend-cli/template/src/pages/Layout/index.tsx
  10. 141 0
      packages/backend-cli/template/src/pages/User/components/UserAdd/_withoutRole.tsx
  11. 174 141
      packages/backend-cli/template/src/pages/User/components/UserAdd/index.tsx
  12. 74 0
      packages/backend-cli/template/src/pages/User/role.tsx
  13. 158 0
      packages/backend-cli/template/src/pages/User/roleEdit.tsx
  14. 22 0
      packages/backend-cli/template/src/router/_withoutRole.tsx
  15. 26 1
      packages/backend-cli/template/src/router/index.tsx
  16. 18 16
      packages/backend-cli/template/src/router/types.ts
  17. 22 22
      packages/backend-cli/template/src/store/reducer/base.ts
  18. 36 30
      packages/backend-cli/template/src/types/index.ts
  19. 52 31
      packages/backend-cli/template/src/types/user.ts
  20. 47 9
      packages/backend-cli/template/src/utils/index.ts
  21. 46 43
      packages/docs/docs/index.md
  22. 8 2
      packages/docs/docs/log/BACKEND-CLI_CHANGELOG.md
  23. 200 13
      pnpm-lock.yaml

+ 8 - 2
packages/backend-cli/CHANGELOG.md

@@ -1,5 +1,11 @@
 # @dage/backend-cli
 
+## 2.1.0
+
+### Minor Changes
+
+- 新增角色管理
+
 ## 1.2.6
 
 ### Patch Changes
@@ -17,8 +23,8 @@
 
 ### Patch Changes
 
-- - 修改密码-确认密码新增必填符号
-  - request meta 新增 showError
+- 修改密码-确认密码新增必填符号
+- request meta 新增 showError
 
 ## 1.2.1
 

+ 76 - 32
packages/backend-cli/create-dage-backend-cli

@@ -14,7 +14,9 @@ process.stdout.setEncoding("utf8");
 program
   .version(pkg.version)
   .arguments("<projectName>")
-  .action(async (projectName) => {
+  .option("--no-role", "Skip role management feature")
+  .action(async (projectName, options) => {
+    const enableRole = options.role;
     const projectDir = path.join(process.cwd(), projectName);
 
     if (fs.existsSync(projectDir)) {
@@ -25,7 +27,6 @@ program
     fs.mkdirSync(projectDir);
 
     const templateDir = path.join(__dirname, "template");
-    const exclusions = ["node_modules", "pnpm-lock.yaml"];
 
     const files = await promisify(fs.readdir)(templateDir);
     const progressBar = new cliProgress.SingleBar({
@@ -36,36 +37,13 @@ program
     });
     progressBar.start(files.length, 0);
 
-    for (const file of files) {
-      if (!exclusions.includes(file)) {
-        const srcPath = path.join(templateDir, file);
-        const destPath = path.join(projectDir, file);
-        await ncp(srcPath, destPath);
-
-        if (file === "package.json") {
-          const packageJsonPath = path.join(destPath);
-          const packageJson = JSON.parse(
-            fs.readFileSync(packageJsonPath, "utf8")
-          );
-
-          packageJson.name = projectName;
-          packageJson.version = pkg.version;
-
-          Object.keys(packageJson.dependencies).forEach((key) => {
-            if (key.startsWith("@dage")) {
-              packageJson.dependencies[key] = "^" + getPackageVersion(key);
-            }
-          });
-
-          fs.writeFileSync(
-            packageJsonPath,
-            JSON.stringify(packageJson, null, 2)
-          );
-        }
-      }
-
-      progressBar.increment();
-    }
+    await copyTemplateFiles(
+      templateDir,
+      projectDir,
+      enableRole,
+      progressBar,
+      projectName
+    );
 
     progressBar.stop();
 
@@ -121,3 +99,69 @@ function getPackageVersion(pkgName) {
   }
   return version;
 }
+
+async function copyTemplateFiles(
+  src,
+  dest,
+  enableRole,
+  progressBar,
+  projectName
+) {
+  const entries = await fs.promises.readdir(src, { withFileTypes: true });
+  const exclusions = [
+    "build",
+    "node_modules",
+    "package-lock.json",
+    "_withoutRole.tsx",
+  ];
+
+  for (const entry of entries) {
+    if (exclusions.includes(entry.name)) continue;
+
+    const srcPath = path.join(src, entry.name);
+    const destPath = path.join(dest, entry.name);
+
+    if (entry.isDirectory()) {
+      await fs.promises.mkdir(destPath, { recursive: true });
+      await copyTemplateFiles(srcPath, destPath, enableRole, progressBar);
+    } else {
+      if (
+        srcPath.includes("src\\router\\index.tsx") ||
+        srcPath.includes("src\\pages\\Layout\\index.tsx") ||
+        srcPath.includes("src\\pages\\User\\components\\UserAdd\\index.tsx")
+      ) {
+        const routeContent = enableRole
+          ? await fs.promises.readFile(path.join(src, "index.tsx"), "utf8")
+          : await fs.promises.readFile(
+              path.join(src, "_withoutRole.tsx"),
+              "utf8"
+            );
+
+        await fs.promises.writeFile(destPath, routeContent);
+      } else {
+        await ncp(srcPath, destPath);
+      }
+
+      // 处理 package.json(保留原有逻辑)
+      if (entry.name === "package.json") {
+        const packageJsonPath = path.join(destPath);
+        const packageJson = JSON.parse(
+          fs.readFileSync(packageJsonPath, "utf8")
+        );
+
+        packageJson.name = projectName;
+        packageJson.version = pkg.version;
+
+        Object.keys(packageJson.dependencies).forEach((key) => {
+          if (key.startsWith("@dage")) {
+            packageJson.dependencies[key] = "^" + getPackageVersion(key);
+          }
+        });
+
+        fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
+      }
+
+      progressBar.increment();
+    }
+  }
+}

+ 1 - 1
packages/backend-cli/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@dage/backend-cli",
-  "version": "2.0.0",
+  "version": "2.1.0",
   "description": "创建后台模板",
   "main": "index.js",
   "files": [

+ 5 - 0
packages/backend-cli/template/.eslintrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+  rules: {
+    "react-hooks/exhaustive-deps": "off",
+  },
+};

+ 7 - 2
packages/backend-cli/template/package.json

@@ -17,6 +17,7 @@
     "@testing-library/react": "^16.2.0",
     "@testing-library/user-event": "^13.5.0",
     "@types/jest": "^27.5.2",
+    "@types/lodash": "^4.14.197",
     "@types/node": "^16.18.126",
     "@types/react": "^19.0.11",
     "@types/react-dom": "^19.0.4",
@@ -44,6 +45,7 @@
     "jest": "^27.4.3",
     "jest-resolve": "^27.4.2",
     "jest-watch-typeahead": "^1.0.0",
+    "lodash": "^4.17.21",
     "mini-css-extract-plugin": "^2.4.5",
     "path-to-regexp": "^8.2.0",
     "postcss": "^8.4.4",
@@ -76,8 +78,8 @@
     "workbox-webpack-plugin": "^6.4.1"
   },
   "scripts": {
-    "start": "cross-env REACT_APP_API_URL=https://sit-shoubodyh.4dage.com node scripts/start.js",
-    "build": "cross-env PUBLIC_URL=./ REACT_APP_API_URL=https://sit-shoubodyh.4dage.com node scripts/build.js",
+    "start": "cross-env REACT_APP_API_URL=https://sit-shoubodyh.4dage.com REACT_APP_IMG_PUBLIC= node scripts/start.js",
+    "build": "cross-env PUBLIC_URL=./ REACT_APP_API_URL=https://sit-shoubodyh.4dage.com REACT_APP_IMG_PUBLIC= node scripts/build.js",
     "test": "node scripts/test.js"
   },
   "eslintConfig": {
@@ -151,5 +153,8 @@
     "presets": [
       "react-app"
     ]
+  },
+  "devDependencies": {
+    "sass": "^1.86.0"
   }
 }

+ 44 - 23
packages/backend-cli/template/src/api/user.ts

@@ -1,23 +1,44 @@
-import { GetUserListParams, SaveUserType } from "@/types";
-import { requestByGet, requestByPost } from "@dage/service";
-
-export const userApi = {
-  getList(params: GetUserListParams) {
-    return requestByPost("/api/sys/user/list", params);
-  },
-  handleType(id: number, isEnabled: number) {
-    return requestByGet(`/api/sys/user/editStatus/${id}/${isEnabled}`);
-  },
-  del(ids: number) {
-    return requestByGet(`/api/sys/user/removes/${ids}`);
-  },
-  resetPwd(id: number) {
-    return requestByGet(`/api/sys/user/resetPass/${id}`);
-  },
-  getDetail(id: number) {
-    return requestByGet(`/api/sys/user/detail/${id}`);
-  },
-  edit(params: SaveUserType) {
-    return requestByPost("/api/sys/user/save", params);
-  },
-};
+import {
+  GetUserListParams,
+  IRoleItem,
+  PermItemType,
+  SaveRoleParams,
+  SaveUserType,
+} from "@/types";
+import { requestByGet, requestByPost } from "@dage/service";
+
+export const userApi = {
+  getList(params: GetUserListParams) {
+    return requestByPost("/api/sys/user/list", params);
+  },
+  getRoleList() {
+    return requestByGet<IRoleItem[]>("/api/sys/user/getRole");
+  },
+  getPermTree() {
+    return requestByGet<PermItemType[]>("/api/sys/user/perm/getTree");
+  },
+  saveRole(params: SaveRoleParams) {
+    return requestByPost("/api/sys/role/save", params);
+  },
+  deleteRole(id: number | string) {
+    return requestByGet(`/api/sys/role/remove/${id}`);
+  },
+  getRole(id: number | string) {
+    return requestByGet(`/api/sys/role/detail/${id}`);
+  },
+  handleType(id: number, isEnabled: number) {
+    return requestByGet(`/api/sys/user/editStatus/${id}/${isEnabled}`);
+  },
+  del(ids: number) {
+    return requestByGet(`/api/sys/user/removes/${ids}`);
+  },
+  resetPwd(id: number) {
+    return requestByGet(`/api/sys/user/resetPass/${id}`);
+  },
+  getDetail(id: number) {
+    return requestByGet(`/api/sys/user/detail/${id}`);
+  },
+  edit(params: SaveUserType) {
+    return requestByPost("/api/sys/user/save", params);
+  },
+};

+ 129 - 0
packages/backend-cli/template/src/pages/Layout/_withoutRole.tsx

@@ -0,0 +1,129 @@
+import React, { useMemo, useEffect, Suspense } from "react";
+import { App, Layout } from "antd";
+import { useSelector } from "react-redux";
+import { Route, Routes, useNavigate, Navigate } from "react-router-dom";
+import { Content } from "antd/es/layout/layout";
+import { hasToken, getTokenInfo, DageLoading } from "@dage/pc-components";
+import store from "@/store";
+import { LayoutMenu, LayoutHeader } from "./components";
+import { RootState } from "@/store";
+import LogoImage from "@/assets/images/logo.png";
+import { DEFAULT_ADMIN_MENU, DEFAULT_MENU, DageRouteItem } from "@/router";
+import "./index.scss";
+
+const NotFound = React.lazy(() => import("@/components/NotFound"));
+
+export default function CustomLayout() {
+  const navigate = useNavigate();
+  const baseStore = useSelector<RootState, RootState["base"]>(
+    (state) => state.base
+  );
+  const menuList = useMemo<DageRouteItem[]>(() => {
+    return baseStore.userInfo?.user.isAdmin
+      ? [...DEFAULT_MENU, ...DEFAULT_ADMIN_MENU]
+      : [...DEFAULT_MENU];
+  }, [baseStore.userInfo]);
+
+  useEffect(() => {
+    if (!hasToken()) {
+      navigate("/login", {
+        replace: true,
+      });
+    } else {
+      store.dispatch({ type: "setUserInfo", payload: getTokenInfo() });
+    }
+  }, []);
+
+  return (
+    <App>
+      <Layout hasSider className="layout">
+        {/* 菜单 */}
+        <Layout.Sider
+          width={220}
+          style={{
+            position: "fixed",
+            top: 0,
+            left: 0,
+            bottom: 0,
+            background: "var(--primary-color)",
+          }}
+        >
+          <div className="logo">
+            <img draggable="false" alt="logo" src={LogoImage} />
+          </div>
+
+          <LayoutMenu
+            className="layout-menu"
+            theme="dark"
+            inlineIndent={20}
+            menuData={menuList}
+          />
+        </Layout.Sider>
+
+        <Layout style={{ marginLeft: 220 }}>
+          {/* 头部 */}
+          <LayoutHeader menuData={menuList} />
+
+          {/* 主体 */}
+          <Content
+            style={{
+              margin: "15px",
+              overflow: "initial",
+              position: "relative",
+              background: "#ffffff",
+              padding: 20,
+              borderRadius: 4,
+            }}
+          >
+            <Suspense fallback={<DageLoading />}>
+              {menuList.length && (
+                <Routes>
+                  <Route
+                    path="/"
+                    element={
+                      <Navigate to={menuList[0].redirect || menuList[0].path} />
+                    }
+                  />
+                  {renderRoutes(menuList).map((menu) =>
+                    menu.redirect ? (
+                      <Route
+                        key={menu.path}
+                        path={menu.path}
+                        element={<Navigate replace to={menu.redirect} />}
+                      />
+                    ) : (
+                      <Route
+                        key={menu.path}
+                        path={menu.path}
+                        Component={menu.Component}
+                      />
+                    )
+                  )}
+                  <Route path="*" Component={NotFound} />
+                </Routes>
+              )}
+            </Suspense>
+          </Content>
+        </Layout>
+      </Layout>
+    </App>
+  );
+}
+
+function renderRoutes(routes: DageRouteItem[]) {
+  function deep(v: DageRouteItem[]) {
+    const stack: DageRouteItem[] = [];
+    v.forEach((item) => {
+      const { children = [], ...rest } = item;
+
+      stack.push(rest);
+
+      if (!!children.length) {
+        stack.push(...deep(children));
+      }
+    });
+    return stack;
+  }
+
+  return deep(routes);
+}

+ 2 - 1
packages/backend-cli/template/src/pages/Layout/components/Header/index.tsx

@@ -3,7 +3,7 @@ import style from "./index.module.scss";
 import { App, Avatar, Breadcrumb, Button, Popover } from "antd";
 import { Header } from "antd/es/layout/layout";
 import { useSelector } from "react-redux";
-import { RootState } from "@/store";
+import store, { RootState } from "@/store";
 import { ResetPassword } from "./components/ResetPassword";
 import { logout } from "@/utils";
 import { DageRouteItem } from "@/router";
@@ -68,6 +68,7 @@ export const LayoutHeader: FC<LayoutHeaderProps> = ({ menuData }) => {
       content: "确定退出吗?",
       async onOk() {
         await logout();
+        store.dispatch({ type: "setUserInfo", payload: null });
       },
     });
   }, [modal]);

+ 190 - 129
packages/backend-cli/template/src/pages/Layout/index.tsx

@@ -1,129 +1,190 @@
-import React, { useMemo, useEffect, Suspense } from "react";
-import { App, Layout } from "antd";
-import { useSelector } from "react-redux";
-import { Route, Routes, useNavigate, Navigate } from "react-router-dom";
-import { Content } from "antd/es/layout/layout";
-import { hasToken, getTokenInfo, DageLoading } from "@dage/pc-components";
-import store from "@/store";
-import { LayoutMenu, LayoutHeader } from "./components";
-import { RootState } from "@/store";
-import LogoImage from "@/assets/images/logo.png";
-import { DEFAULT_ADMIN_MENU, DEFAULT_MENU, DageRouteItem } from "@/router";
-import "./index.scss";
-
-const NotFound = React.lazy(() => import("@/components/NotFound"));
-
-export default function CustomLayout() {
-  const navigate = useNavigate();
-  const baseStore = useSelector<RootState, RootState["base"]>(
-    (state) => state.base
-  );
-  const menuList = useMemo<DageRouteItem[]>(() => {
-    return baseStore.userInfo?.user.isAdmin
-      ? [...DEFAULT_MENU, ...DEFAULT_ADMIN_MENU]
-      : [...DEFAULT_MENU];
-  }, [baseStore.userInfo]);
-
-  useEffect(() => {
-    if (!hasToken()) {
-      navigate("/login", {
-        replace: true,
-      });
-    } else {
-      store.dispatch({ type: "setUserInfo", payload: getTokenInfo() });
-    }
-  }, [navigate]);
-
-  return (
-    <App>
-      <Layout hasSider className="layout">
-        {/* 菜单 */}
-        <Layout.Sider
-          width={220}
-          style={{
-            position: "fixed",
-            top: 0,
-            left: 0,
-            bottom: 0,
-            background: "var(--primary-color)",
-          }}
-        >
-          <div className="logo">
-            <img draggable="false" alt="logo" src={LogoImage} />
-          </div>
-
-          <LayoutMenu
-            className="layout-menu"
-            theme="dark"
-            inlineIndent={20}
-            menuData={menuList}
-          />
-        </Layout.Sider>
-
-        <Layout style={{ marginLeft: 220 }}>
-          {/* 头部 */}
-          <LayoutHeader menuData={menuList} />
-
-          {/* 主体 */}
-          <Content
-            style={{
-              margin: "15px",
-              overflow: "initial",
-              position: "relative",
-              background: "#ffffff",
-              padding: 20,
-              borderRadius: 4,
-            }}
-          >
-            <Suspense fallback={<DageLoading />}>
-              {menuList.length && (
-                <Routes>
-                  <Route
-                    path="/"
-                    element={
-                      <Navigate to={menuList[0].redirect || menuList[0].path} />
-                    }
-                  />
-                  {renderRoutes(menuList).map((menu) =>
-                    menu.redirect ? (
-                      <Route
-                        key={menu.path}
-                        path={menu.path}
-                        element={<Navigate replace to={menu.redirect} />}
-                      />
-                    ) : (
-                      <Route
-                        key={menu.path}
-                        path={menu.path}
-                        Component={menu.Component}
-                      />
-                    )
-                  )}
-                  <Route path="*" Component={NotFound} />
-                </Routes>
-              )}
-            </Suspense>
-          </Content>
-        </Layout>
-      </Layout>
-    </App>
-  );
-}
-
-function renderRoutes(routes: DageRouteItem[]) {
-  function deep(v: DageRouteItem[]) {
-    const stack: DageRouteItem[] = [];
-    v.forEach((item) => {
-      const { children = [], ...rest } = item;
-
-      stack.push(rest);
-
-      if (!!children.length) {
-        stack.push(...deep(children));
-      }
-    });
-    return stack;
-  }
-
-  return deep(routes);
-}
+import React, { useMemo, useEffect, Suspense, useState } from "react";
+import { App, Layout } from "antd";
+import { useSelector } from "react-redux";
+import { Route, Routes, useNavigate, Navigate } from "react-router-dom";
+import { Content } from "antd/es/layout/layout";
+import { hasToken, getTokenInfo, DageLoading } from "@dage/pc-components";
+import store from "@/store";
+import { LayoutMenu, LayoutHeader } from "./components";
+import { RootState } from "@/store";
+import { userApi } from "@/api";
+import { getAuthorizedIds } from "@/utils";
+import { YES_OR_NO } from "@/types";
+import LogoImage from "@/assets/images/logo.png";
+import { DEFAULT_ADMIN_MENU, DEFAULT_MENU, DageRouteItem } from "@/router";
+import "./index.scss";
+
+const NotFound = React.lazy(() => import("@/components/NotFound"));
+
+export default function CustomLayout() {
+  const navigate = useNavigate();
+  const baseStore = useSelector<RootState, RootState["base"]>(
+    (state) => state.base
+  );
+  const [menuLoading, setMenuLoading] = useState(false);
+  const [menuList, setMenuList] = useState<DageRouteItem[]>([]);
+
+  const getPermissions = async () => {
+    try {
+      setMenuLoading(true);
+      const data = await userApi.getRole(baseStore.userInfo!.user.roleId);
+      const permissonIds = getAuthorizedIds(data.permission);
+      const menus = filterAuthorizedRoutes(DEFAULT_MENU, permissonIds);
+      setMenuList(menus);
+      const target = getFirstPath(menus);
+      target && navigate(target);
+    } finally {
+      setMenuLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    if (!baseStore.userInfo) return;
+
+    if (baseStore.userInfo?.user.isAdmin === YES_OR_NO.YES) {
+      setMenuList([...DEFAULT_MENU, ...DEFAULT_ADMIN_MENU]);
+    } else {
+      getPermissions();
+    }
+  }, [baseStore]);
+
+  useEffect(() => {
+    if (!hasToken()) {
+      navigate("/login", {
+        replace: true,
+      });
+    } else {
+      store.dispatch({ type: "setUserInfo", payload: getTokenInfo() });
+    }
+  }, []);
+
+  return (
+    <App>
+      {menuLoading ? (
+        <DageLoading />
+      ) : (
+        <Layout hasSider className="layout">
+          {/* 菜单 */}
+          <Layout.Sider
+            width={220}
+            style={{
+              position: "fixed",
+              top: 0,
+              left: 0,
+              bottom: 0,
+              background: "var(--primary-color)",
+            }}
+          >
+            <div className="logo">
+              <img draggable="false" alt="logo" src={LogoImage} />
+            </div>
+
+            <LayoutMenu
+              className="layout-menu"
+              theme="dark"
+              inlineIndent={20}
+              menuData={menuList}
+            />
+          </Layout.Sider>
+
+          <Layout style={{ marginLeft: 220 }}>
+            {/* 头部 */}
+            <LayoutHeader menuData={menuList} />
+
+            {/* 主体 */}
+            <Content
+              style={{
+                margin: "15px",
+                overflow: "initial",
+                position: "relative",
+                background: "#ffffff",
+                padding: 20,
+                borderRadius: 4,
+              }}
+            >
+              <Suspense fallback={<DageLoading />}>
+                {menuList.length && (
+                  <Routes>
+                    <Route
+                      path="/"
+                      element={
+                        <Navigate
+                          to={menuList[0].redirect || menuList[0].path}
+                        />
+                      }
+                    />
+                    {renderRoutes(menuList).map((menu) =>
+                      menu.redirect ? (
+                        <Route
+                          key={menu.path}
+                          path={menu.path}
+                          element={<Navigate replace to={menu.redirect} />}
+                        />
+                      ) : (
+                        <Route
+                          key={menu.path}
+                          path={menu.path}
+                          Component={menu.Component}
+                        />
+                      )
+                    )}
+                    <Route path="*" Component={NotFound} />
+                  </Routes>
+                )}
+              </Suspense>
+            </Content>
+          </Layout>
+        </Layout>
+      )}
+    </App>
+  );
+}
+
+function renderRoutes(routes: DageRouteItem[]) {
+  function deep(v: DageRouteItem[]) {
+    const stack: DageRouteItem[] = [];
+    v.forEach((item) => {
+      const { children = [], ...rest } = item;
+
+      stack.push(rest);
+
+      if (!!children.length) {
+        stack.push(...deep(children));
+      }
+    });
+    return stack;
+  }
+
+  return deep(routes);
+}
+
+function filterAuthorizedRoutes(routes: DageRouteItem[], permIds: number[]) {
+  const filterRoutes = (routeList: DageRouteItem[]): DageRouteItem[] => {
+    return routeList
+      .map((route) => {
+        const newRoute = { ...route };
+
+        if (newRoute.children) {
+          newRoute.children = filterRoutes(newRoute.children);
+        }
+
+        const shouldKeep = permIds.includes(route.mapId || -1);
+
+        return shouldKeep ? newRoute : null;
+      })
+      .filter(Boolean) as DageRouteItem[];
+  };
+
+  return filterRoutes(routes);
+}
+
+function getFirstPath(menus: DageRouteItem[]): string | null {
+  if (!menus.length) return null;
+
+  const item = menus[0];
+  if (item.children) {
+    if (item.children.every((i) => i.hide)) return item.path;
+    return getFirstPath(item.children);
+  }
+  return item.path;
+}

+ 141 - 0
packages/backend-cli/template/src/pages/User/components/UserAdd/_withoutRole.tsx

@@ -0,0 +1,141 @@
+import { SaveUserType } from "@/types";
+import {
+  Button,
+  Form,
+  FormInstance,
+  Input,
+  Modal,
+  Popconfirm,
+  message,
+} from "antd";
+import React, { useCallback, useEffect, useRef } from "react";
+import styles from "./index.module.scss";
+import { userApi } from "@/api";
+
+type Props = {
+  id: any;
+  closePage: () => void;
+  upTableList: () => void;
+  addTableList: () => void;
+};
+
+function UserAdd({ id, closePage, upTableList, addTableList }: Props) {
+  // 设置表单初始数据(区分编辑和新增)
+  const FormBoxRef = useRef<FormInstance>(null);
+
+  const getInfoInAPIFu = useCallback(async (id: number) => {
+    const data = await userApi.getDetail(id);
+    FormBoxRef.current?.setFieldsValue(data);
+    console.log("是编辑,在这里发请求拿数据", data);
+  }, []);
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    // return MessageFu.warning("有表单不符号规则!");
+  }, []);
+
+  useEffect(() => {
+    if (id) getInfoInAPIFu(id);
+    else {
+      FormBoxRef.current?.setFieldsValue({});
+    }
+  }, [getInfoInAPIFu, id]);
+
+  // 通过校验点击确定
+  const onFinish = useCallback(
+    async (values: any) => {
+      const obj: SaveUserType = {
+        ...values,
+        id: id ? id : null,
+      };
+
+      await userApi.edit(obj);
+
+      message.success(id ? "编辑成功!" : "新增成功!");
+      if (id) upTableList();
+      else addTableList();
+
+      closePage();
+      console.log("通过校验,点击确定");
+    },
+    [addTableList, closePage, id, upTableList]
+  );
+
+  return (
+    <Modal
+      wrapClassName={styles.userAdd}
+      destroyOnClose
+      open={true}
+      title={id ? "编辑用户" : "新增用户"}
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className="userAddMain">
+        <Form
+          ref={FormBoxRef}
+          name="basic"
+          labelCol={{ span: 5 }}
+          onFinish={onFinish}
+          onFinishFailed={onFinishFailed}
+          autoComplete="off"
+        >
+          <Form.Item
+            label="账号名"
+            name="userName"
+            rules={[{ required: true, message: "请输入账号名!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input
+              disabled={id}
+              maxLength={15}
+              showCount
+              placeholder="请输入内容"
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="用户昵称"
+            name="nickName"
+            rules={[{ required: true, message: "请输入用户昵称!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={8} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item
+            label="真实姓名"
+            name="realName"
+            rules={[{ required: true, message: "请输入真实姓名!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={8} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          {id ? null : <div className="passTit">* 默认密码 123456</div>}
+
+          {/* 确定和取消按钮 */}
+          <br />
+          <Form.Item wrapperCol={{ offset: 9, span: 16 }}>
+            <Button type="primary" htmlType="submit">
+              提交
+            </Button>
+            &emsp;
+            <Popconfirm
+              title="放弃编辑后,信息将不会保存!"
+              okText="放弃"
+              cancelText="取消"
+              onConfirm={closePage}
+            >
+              <Button>取消</Button>
+            </Popconfirm>
+          </Form.Item>
+        </Form>
+      </div>
+    </Modal>
+  );
+}
+
+const MemoUserAdd = React.memo(UserAdd);
+
+export default MemoUserAdd;

+ 174 - 141
packages/backend-cli/template/src/pages/User/components/UserAdd/index.tsx

@@ -1,141 +1,174 @@
-import { SaveUserType } from "@/types";
-import {
-  Button,
-  Form,
-  FormInstance,
-  Input,
-  Modal,
-  Popconfirm,
-  message,
-} from "antd";
-import React, { useCallback, useEffect, useRef } from "react";
-import styles from "./index.module.scss";
-import { userApi } from "@/api";
-
-type Props = {
-  id: any;
-  closePage: () => void;
-  upTableList: () => void;
-  addTableList: () => void;
-};
-
-function UserAdd({ id, closePage, upTableList, addTableList }: Props) {
-  // 设置表单初始数据(区分编辑和新增)
-  const FormBoxRef = useRef<FormInstance>(null);
-
-  const getInfoInAPIFu = useCallback(async (id: number) => {
-    const data = await userApi.getDetail(id);
-    FormBoxRef.current?.setFieldsValue(data);
-    console.log("是编辑,在这里发请求拿数据", data);
-  }, []);
-
-  // 没有通过校验
-  const onFinishFailed = useCallback(() => {
-    // return MessageFu.warning("有表单不符号规则!");
-  }, []);
-
-  useEffect(() => {
-    if (id) getInfoInAPIFu(id);
-    else {
-      FormBoxRef.current?.setFieldsValue({});
-    }
-  }, [getInfoInAPIFu, id]);
-
-  // 通过校验点击确定
-  const onFinish = useCallback(
-    async (values: any) => {
-      const obj: SaveUserType = {
-        ...values,
-        id: id ? id : null,
-      };
-
-      await userApi.edit(obj);
-
-      message.success(id ? "编辑成功!" : "新增成功!");
-      if (id) upTableList();
-      else addTableList();
-
-      closePage();
-      console.log("通过校验,点击确定");
-    },
-    [addTableList, closePage, id, upTableList]
-  );
-
-  return (
-    <Modal
-      wrapClassName={styles.userAdd}
-      destroyOnClose
-      open={true}
-      title={id ? "编辑用户" : "新增用户"}
-      footer={
-        [] // 设置footer为空,去掉 取消 确定默认按钮
-      }
-    >
-      <div className="userAddMain">
-        <Form
-          ref={FormBoxRef}
-          name="basic"
-          labelCol={{ span: 5 }}
-          onFinish={onFinish}
-          onFinishFailed={onFinishFailed}
-          autoComplete="off"
-        >
-          <Form.Item
-            label="账号名"
-            name="userName"
-            rules={[{ required: true, message: "请输入账号名!" }]}
-            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
-          >
-            <Input
-              disabled={id}
-              maxLength={15}
-              showCount
-              placeholder="请输入内容"
-            />
-          </Form.Item>
-
-          <Form.Item
-            label="用户昵称"
-            name="nickName"
-            rules={[{ required: true, message: "请输入用户昵称!" }]}
-            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
-          >
-            <Input maxLength={8} showCount placeholder="请输入内容" />
-          </Form.Item>
-
-          <Form.Item
-            label="真实姓名"
-            name="realName"
-            rules={[{ required: true, message: "请输入真实姓名!" }]}
-            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
-          >
-            <Input maxLength={8} showCount placeholder="请输入内容" />
-          </Form.Item>
-
-          {id ? null : <div className="passTit">* 默认密码 123456</div>}
-
-          {/* 确定和取消按钮 */}
-          <br />
-          <Form.Item wrapperCol={{ offset: 9, span: 16 }}>
-            <Button type="primary" htmlType="submit">
-              提交
-            </Button>
-            &emsp;
-            <Popconfirm
-              title="放弃编辑后,信息将不会保存!"
-              okText="放弃"
-              cancelText="取消"
-              onConfirm={closePage}
-            >
-              <Button>取消</Button>
-            </Popconfirm>
-          </Form.Item>
-        </Form>
-      </div>
-    </Modal>
-  );
-}
-
-const MemoUserAdd = React.memo(UserAdd);
-
-export default MemoUserAdd;
+import { IRoleItem, SaveUserType } from "@/types";
+import {
+  Button,
+  Form,
+  FormInstance,
+  Input,
+  Modal,
+  Popconfirm,
+  Select,
+  message,
+} from "antd";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import styles from "./index.module.scss";
+import { userApi } from "@/api";
+
+type Props = {
+  id: any;
+  closePage: () => void;
+  upTableList: () => void;
+  addTableList: () => void;
+};
+
+function UserAdd({ id, closePage, upTableList, addTableList }: Props) {
+  // 设置表单初始数据(区分编辑和新增)
+  const FormBoxRef = useRef<FormInstance>(null);
+  const [roleLoading, setRoleLoading] = useState(false);
+  const [roleList, setRoleList] = useState<IRoleItem[]>([]);
+
+  const getInfoInAPIFu = useCallback(async (id: number) => {
+    const data = await userApi.getDetail(id);
+    FormBoxRef.current?.setFieldsValue(data);
+    console.log("是编辑,在这里发请求拿数据", data);
+  }, []);
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    // return MessageFu.warning("有表单不符号规则!");
+  }, []);
+
+  useEffect(() => {
+    if (id) getInfoInAPIFu(id);
+    else {
+      FormBoxRef.current?.setFieldsValue({});
+    }
+  }, [getInfoInAPIFu, id]);
+
+  // 通过校验点击确定
+  const onFinish = useCallback(
+    async (values: any) => {
+      const obj: SaveUserType = {
+        ...values,
+        id: id ? id : null,
+      };
+
+      await userApi.edit(obj);
+
+      message.success(id ? "编辑成功!" : "新增成功!");
+      if (id) upTableList();
+      else addTableList();
+
+      closePage();
+      console.log("通过校验,点击确定");
+    },
+    [addTableList, closePage, id, upTableList]
+  );
+
+  const getRoleList = async () => {
+    try {
+      setRoleLoading(true);
+      const data = await userApi.getRoleList();
+      setRoleList(data);
+    } finally {
+      setRoleLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    getRoleList();
+  }, []);
+
+  return (
+    <Modal
+      wrapClassName={styles.userAdd}
+      destroyOnClose
+      open={true}
+      title={id ? "编辑用户" : "新增用户"}
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className="userAddMain">
+        <Form
+          ref={FormBoxRef}
+          name="basic"
+          labelCol={{ span: 5 }}
+          onFinish={onFinish}
+          onFinishFailed={onFinishFailed}
+          autoComplete="off"
+        >
+          <Form.Item
+            label="账号名"
+            name="userName"
+            rules={[{ required: true, message: "请输入账号名!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input
+              disabled={id}
+              maxLength={15}
+              showCount
+              placeholder="请输入内容"
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="用户昵称"
+            name="nickName"
+            rules={[{ required: true, message: "请输入用户昵称!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={8} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item
+            label="真实姓名"
+            name="realName"
+            rules={[{ required: true, message: "请输入真实姓名!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={8} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item
+            label="用户角色"
+            name="roleId"
+            rules={[{ required: true, message: "请选择用户角色!" }]}
+          >
+            <Select
+              loading={roleLoading}
+              options={roleList}
+              fieldNames={{
+                label: "roleName",
+                value: "id",
+              }}
+              placeholder="请选择"
+            />
+          </Form.Item>
+
+          {id ? null : <div className="passTit">* 默认密码 123456</div>}
+
+          {/* 确定和取消按钮 */}
+          <br />
+          <Form.Item wrapperCol={{ offset: 9, span: 16 }}>
+            <Button type="primary" htmlType="submit">
+              提交
+            </Button>
+            &emsp;
+            <Popconfirm
+              title="放弃编辑后,信息将不会保存!"
+              okText="放弃"
+              cancelText="取消"
+              onConfirm={closePage}
+            >
+              <Button>取消</Button>
+            </Popconfirm>
+          </Form.Item>
+        </Form>
+      </div>
+    </Modal>
+  );
+}
+
+const MemoUserAdd = React.memo(UserAdd);
+
+export default MemoUserAdd;

+ 74 - 0
packages/backend-cli/template/src/pages/User/role.tsx

@@ -0,0 +1,74 @@
+import { userApi } from "@/api";
+import { IRoleItem } from "@/types";
+import { Button, Table } from "antd";
+import { DageTableActions } from "@dage/pc-components";
+import { useEffect, useState } from "react";
+import { PlusOutlined } from "@ant-design/icons";
+import { useNavigate } from "react-router-dom";
+
+export default function RolePage() {
+  const [list, setList] = useState<IRoleItem[]>([]);
+  const [loading, setLoading] = useState(false);
+  const navigate = useNavigate();
+
+  const getList = async () => {
+    setLoading(true);
+    try {
+      const data = await userApi.getRoleList();
+      setList(data);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleDelete = async (id: number) => {
+    await userApi.deleteRole(id);
+    getList();
+  };
+
+  useEffect(() => {
+    getList();
+  }, []);
+
+  return (
+    <div className="role">
+      <Button
+        type="primary"
+        icon={<PlusOutlined />}
+        onClick={() => navigate("/setting/role/create")}
+      >
+        新增
+      </Button>
+
+      <Table
+        loading={loading}
+        className="page-table"
+        dataSource={list}
+        columns={[
+          {
+            title: "角色名称",
+            dataIndex: "roleName",
+            align: "center",
+          },
+          {
+            title: "角色说明",
+            dataIndex: "roleDesc",
+            align: "center",
+          },
+          {
+            title: "操作",
+            align: "center",
+            render: (val) => (
+              <DageTableActions
+                onEdit={() => navigate(`/setting/role/edit/${val.id}`)}
+                onDelete={handleDelete.bind(undefined, val.id)}
+              />
+            ),
+          },
+        ]}
+        rowKey="id"
+        pagination={false}
+      />
+    </div>
+  );
+}

+ 158 - 0
packages/backend-cli/template/src/pages/User/roleEdit.tsx

@@ -0,0 +1,158 @@
+import { userApi } from "@/api";
+import { FormPageFooter } from "@/components";
+import { PermItemType } from "@/types";
+import { getAuthorizedIds } from "@/utils";
+import { DageLoading } from "@dage/pc-components";
+import { Form, Input, Tree } from "antd";
+import { FC, Key, useEffect, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+
+const { TextArea } = Input;
+
+const getCheckedIds = (treeData: PermItemType[], checkedIds: number[]) => {
+  const fullyCheckedIds: number[] = [];
+  const halfCheckedIds: number[] = [];
+
+  const checkNode = (node: PermItemType): boolean => {
+    let allChildrenChecked = true;
+    let someChildrenChecked = false;
+
+    if (node.children && node.children.length > 0) {
+      for (const child of node.children) {
+        const isChildChecked = checkNode(child);
+        if (!isChildChecked) allChildrenChecked = false;
+        if (isChildChecked) someChildrenChecked = true;
+      }
+    }
+
+    const isNodeExplicitlyChecked = checkedIds.includes(node.id);
+
+    if (
+      isNodeExplicitlyChecked &&
+      (allChildrenChecked || node.children?.length === 0)
+    ) {
+      fullyCheckedIds.push(node.id);
+      return true;
+    }
+
+    if (
+      (someChildrenChecked && !isNodeExplicitlyChecked) ||
+      (isNodeExplicitlyChecked && !allChildrenChecked)
+    ) {
+      halfCheckedIds.push(node.id);
+      return true;
+    }
+
+    return isNodeExplicitlyChecked;
+  };
+
+  treeData.forEach((node) => checkNode(node));
+  return { fullyCheckedIds, halfCheckedIds };
+};
+
+const RoleEditPage: FC = () => {
+  const [form] = Form.useForm();
+  const params = useParams();
+  const navigate = useNavigate();
+  const [permTree, setPermTree] = useState<PermItemType[]>([]);
+  const [checkedKeys, setCheckedKeys] = useState<Key[]>([]);
+  const [halfCheckedKeys, setHalfCheckedKeys] = useState<Key[]>([]);
+  const [loading, setLoading] = useState(false);
+
+  const getTree = async () => {
+    const data = await userApi.getPermTree();
+    setPermTree(data);
+  };
+
+  const getDetail = async () => {
+    try {
+      setLoading(true);
+      const data = await userApi.getRole(params.id as string);
+      form.setFieldsValue({
+        roleName: data.role.roleName,
+        roleDesc: data.role.roleDesc,
+      });
+
+      const idsRes = getCheckedIds(permTree, getAuthorizedIds(data.permission));
+      setCheckedKeys(idsRes.fullyCheckedIds);
+      setHalfCheckedKeys(idsRes.halfCheckedIds);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSubmit = async () => {
+    if (!(await form.validateFields())) return;
+
+    const val = form.getFieldsValue();
+    await userApi.saveRole({
+      ...val,
+      resources: [...checkedKeys, ...halfCheckedKeys],
+      id: params.id,
+    });
+    navigate(-1);
+  };
+
+  useEffect(() => {
+    getTree();
+  }, []);
+
+  useEffect(() => {
+    if (params.id && permTree.length) getDetail();
+  }, [permTree]);
+
+  return (
+    <div>
+      {loading && <DageLoading />}
+
+      <Form labelCol={{ span: 4 }} form={form}>
+        <Form.Item
+          label="角色名称"
+          name="roleName"
+          required
+          rules={[{ required: true, message: "请输入内容" }]}
+        >
+          <Input
+            className="w450"
+            showCount
+            maxLength={10}
+            placeholder="请输入内容,最多10字;不能重复"
+          />
+        </Form.Item>
+        <Form.Item label="角色说明" name="roleDesc">
+          <TextArea
+            className="w450"
+            showCount
+            style={{ height: 200 }}
+            maxLength={200}
+            placeholder="请输入内容,最多200字;不能重复"
+          />
+        </Form.Item>
+        <Form.Item label="用户权限">
+          <div className="w450">
+            <Tree
+              checkable
+              // @ts-ignore
+              treeData={permTree}
+              checkedKeys={{
+                checked: checkedKeys,
+                halfChecked: halfCheckedKeys,
+              }}
+              fieldNames={{ title: "name", key: "id" }}
+              onCheck={(keys, halfKeys) => {
+                setCheckedKeys(keys as number[]);
+                if (halfKeys.halfCheckedKeys) {
+                  setHalfCheckedKeys(halfKeys.halfCheckedKeys as number[]);
+                }
+              }}
+            />
+          </div>
+        </Form.Item>
+      </Form>
+
+      <FormPageFooter onSubmit={handleSubmit} onCancel={() => navigate(-1)} />
+    </div>
+  );
+};
+
+export default RoleEditPage;

+ 22 - 0
packages/backend-cli/template/src/router/_withoutRole.tsx

@@ -0,0 +1,22 @@
+import { UserOutlined, SettingOutlined } from "@ant-design/icons";
+import React from "react";
+import { DageRouteItem } from "./types";
+
+export const DEFAULT_MENU: DageRouteItem[] = [];
+
+export const DEFAULT_ADMIN_MENU: DageRouteItem[] = [
+  {
+    path: "/user",
+    title: "用户管理",
+    icon: <UserOutlined />,
+    Component: React.lazy(() => import("../pages/User")),
+  },
+  {
+    path: "/log",
+    title: "操作日志",
+    icon: <SettingOutlined />,
+    Component: React.lazy(() => import("../pages/Log")),
+  },
+];
+
+export * from "./types";

+ 26 - 1
packages/backend-cli/template/src/router/index.tsx

@@ -1,7 +1,12 @@
-import { UserOutlined, SettingOutlined } from "@ant-design/icons";
+import {
+  UserOutlined,
+  SettingOutlined,
+  IdcardOutlined,
+} from "@ant-design/icons";
 import React from "react";
 import { DageRouteItem } from "./types";
 
+// 需要将菜单栏告诉后端录入数据库,然后把id映射到mapId
 export const DEFAULT_MENU: DageRouteItem[] = [];
 
 export const DEFAULT_ADMIN_MENU: DageRouteItem[] = [
@@ -12,6 +17,26 @@ export const DEFAULT_ADMIN_MENU: DageRouteItem[] = [
     Component: React.lazy(() => import("../pages/User")),
   },
   {
+    path: "/role",
+    title: "角色管理",
+    icon: <IdcardOutlined />,
+    Component: React.lazy(() => import("../pages/User/role")),
+    children: [
+      {
+        hide: true,
+        path: "/setting/role/create",
+        title: "新增角色",
+        Component: React.lazy(() => import("../pages/User/roleEdit")),
+      },
+      {
+        hide: true,
+        path: "/setting/role/edit/:id",
+        title: "编辑角色",
+        Component: React.lazy(() => import("../pages/User/roleEdit")),
+      },
+    ],
+  },
+  {
     path: "/log",
     title: "操作日志",
     icon: <SettingOutlined />,

+ 18 - 16
packages/backend-cli/template/src/router/types.ts

@@ -1,16 +1,18 @@
-import { FC, ReactNode } from "react";
-
-export interface DageRouteItem {
-  title: string;
-  path: string;
-  Component?: FC;
-  /** 重定向地址 */
-  redirect?: string;
-  /**
-   * 是否在菜单隐藏
-   * @default false
-   */
-  hide?: boolean;
-  icon?: ReactNode;
-  children?: DageRouteItem[];
-}
+import { FC, ReactNode } from "react";
+
+export interface DageRouteItem {
+  title: string;
+  path: string;
+  Component?: FC;
+  /** 数据库id映射 */
+  mapId?: number;
+  /** 重定向地址 */
+  redirect?: string;
+  /**
+   * 是否在菜单隐藏
+   * @default false
+   */
+  hide?: boolean;
+  icon?: ReactNode;
+  children?: DageRouteItem[];
+}

+ 22 - 22
packages/backend-cli/template/src/store/reducer/base.ts

@@ -1,22 +1,22 @@
-import { LoginResponse } from "@/types";
-
-// 初始化状态
-const initState = {
-  userInfo: null as null | LoginResponse,
-};
-
-// 定义 action 类型
-type BaseActionType = {
-  type: "setUserInfo";
-  payload: LoginResponse;
-};
-
-// 频道 reducer
-export default function baseReducer(state = initState, action: BaseActionType) {
-  switch (action.type) {
-    case "setUserInfo":
-      return { ...state, userInfo: action.payload };
-    default:
-      return state;
-  }
-}
+import { LoginResponse } from "@/types";
+
+// 初始化状态
+const initState = {
+  userInfo: null as null | LoginResponse,
+};
+
+// 定义 action 类型
+type BaseActionType = {
+  type: "setUserInfo";
+  payload: LoginResponse | null;
+};
+
+// 频道 reducer
+export default function baseReducer(state = initState, action: BaseActionType) {
+  switch (action.type) {
+    case "setUserInfo":
+      return { ...state, userInfo: action.payload };
+    default:
+      return state;
+  }
+}

+ 36 - 30
packages/backend-cli/template/src/types/index.ts

@@ -1,30 +1,36 @@
-export interface LoginRequest {
-  userName: string;
-  passWord: string | string[];
-}
-export interface LoginResponse {
-  token: string;
-  user: {
-    id: number;
-    nickName: string;
-    realName: string;
-    phone: string;
-    thumb: string;
-    isAdmin: boolean;
-    isEnabled: boolean;
-  };
-}
-
-export interface UpdatePwdRequest {
-  newPassword: string | string[];
-  oldPassword: string | string[];
-}
-
-export enum ResponseStatusCode {
-  SUCCESS = 0,
-  TOKEN_INVALID = 5001,
-  TOKEN_INVALID2 = 5002,
-}
-
-export * from "./log";
-export * from "./user";
+export interface LoginRequest {
+  userName: string;
+  passWord: string | string[];
+}
+export interface LoginResponse {
+  token: string;
+  user: {
+    id: number;
+    roleId: number;
+    nickName: string;
+    realName: string;
+    phone: string;
+    thumb: string;
+    isAdmin: YES_OR_NO;
+    isEnabled: boolean;
+  };
+}
+
+export interface UpdatePwdRequest {
+  newPassword: string | string[];
+  oldPassword: string | string[];
+}
+
+export enum ResponseStatusCode {
+  SUCCESS = 0,
+  TOKEN_INVALID = 5001,
+  TOKEN_INVALID2 = 5002,
+}
+
+export enum YES_OR_NO {
+  YES = 1,
+  NO = 0,
+}
+
+export * from "./log";
+export * from "./user";

+ 52 - 31
packages/backend-cli/template/src/types/user.ts

@@ -1,31 +1,52 @@
-export type UserTableListType = {
-  createTime: string;
-  creatorId: null;
-  creatorName: string;
-  id: number;
-  isAdmin: number;
-  isEnabled: number;
-  nickName: string;
-  phone: string;
-  realName: string;
-  roleId: null;
-  roleName: string;
-  sex: string;
-  thumb: string;
-  updateTime: string;
-  userName: string;
-};
-
-export interface GetUserListParams {
-  pageNum: number;
-  searchKey?: string;
-  pageSize: number;
-}
-
-export type SaveUserType = {
-  id: number | null;
-  userName: string;
-  nickName: string;
-  roleId: number;
-  realName: string;
-};
+import { YES_OR_NO } from ".";
+
+export type UserTableListType = {
+  createTime: string;
+  creatorId: null;
+  creatorName: string;
+  id: number;
+  isAdmin: YES_OR_NO;
+  isEnabled: number;
+  nickName: string;
+  phone: string;
+  realName: string;
+  roleId: null;
+  roleName: string;
+  sex: string;
+  thumb: string;
+  updateTime: string;
+  userName: string;
+};
+
+export interface GetUserListParams {
+  pageNum: number;
+  searchKey?: string;
+  pageSize: number;
+}
+
+export type SaveUserType = {
+  id: number | null;
+  userName: string;
+  nickName: string;
+  roleId: number;
+  realName: string;
+};
+
+export type PermItemType = {
+  id: number;
+  name: string;
+  authority: boolean;
+  parentId: number | null;
+  children: PermItemType[];
+};
+
+export interface SaveRoleParams {
+  id?: number;
+  resources?: number[];
+  roleDesc?: string;
+  roleName: string;
+}
+
+export interface IRoleItem extends Required<SaveRoleParams> {
+  isEnabled: YES_OR_NO;
+}

+ 47 - 9
packages/backend-cli/template/src/utils/index.ts

@@ -1,9 +1,47 @@
-import { removeTokenInfo } from "@dage/pc-components";
-import { logoutApi } from "@/api";
-
-export const logout = async () => {
-  await logoutApi();
-
-  removeTokenInfo();
-  globalThis.location.href = "#/login";
-};
+import { removeTokenInfo } from "@dage/pc-components";
+import { logoutApi } from "@/api";
+import { PermItemType } from "@/types";
+
+export const logout = async () => {
+  await logoutApi();
+
+  removeTokenInfo();
+  globalThis.location.href = "#/login";
+};
+
+export const getImgFullPath = (path: string) =>
+  `${process.env.REACT_APP_API_URL}${process.env.REACT_APP_IMG_PUBLIC}${path}`;
+
+export const downloadFile = async (url: string, name: string) => {
+  try {
+    const response = await fetch(url);
+    const blob = await response.blob();
+    const newUrl = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = newUrl;
+    link.download = name;
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+  } catch (error) {
+    console.error("下载失败:", error);
+  }
+};
+
+export const getAuthorizedIds = (permItems: PermItemType[]) => {
+  const result: number[] = [];
+  const stack: PermItemType[] = [...permItems];
+
+  while (stack.length > 0) {
+    const item = stack.pop()!;
+    if (item.authority) {
+      result.push(item.id);
+    }
+    if (item.children && item.children.length > 0) {
+      stack.push(...[...item.children].reverse());
+    }
+  }
+
+  return result;
+};

+ 46 - 43
packages/docs/docs/index.md

@@ -1,43 +1,46 @@
----
-hero:
-  title: dage-web-tools
-  desc: web端工具库
-  actions:
-    - text: 开始使用
-      link: /components
-features:
-  - title: "@dage/backend-cli"
-    desc: 基于 `react` 的后台脚手架
-  - title: "@dage/pc-components"
-    desc: 基于 `antd` 的 PC 端[组件库](#/components)
-  - title: "@dage/service"
-    desc: 接口请求[工具](#/service)
-  - title: "@dage/utils"
-    desc: 工具[类库](#/utils)
-  - title: "@dage/krpano"
-    desc: 基于 `krpano` 的 React[组件库](#/krpano)
----
-
-## 如何使用
-
-```bash
-# 安装
-yarn add @dage/pc-components --registry http://192.168.20.245:4873/
-
-# 更新
-yarn upgrade @dage/pc-components --registry http://192.168.20.245:4873/
-```
-
-## 使用 CLI 创建后台模板
-
-```bash
-# 安装
-npm i -g @dage/backend-cli --registry http://192.168.20.245:4873/
-
-# 查看版本号
-create-dage-backend --version
-
-# 创建项目
-# 初始化时优先使用 pnpm -> yarn -> npm
-create-dage-backend packageName
-```
+---
+hero:
+  title: dage-web-tools
+  desc: web端工具库
+  actions:
+    - text: 开始使用
+      link: /components
+features:
+  - title: "@dage/backend-cli"
+    desc: 基于 `react` 的后台脚手架
+  - title: "@dage/pc-components"
+    desc: 基于 `antd` 的 PC 端[组件库](#/components)
+  - title: "@dage/service"
+    desc: 接口请求[工具](#/service)
+  - title: "@dage/utils"
+    desc: 工具[类库](#/utils)
+  - title: "@dage/krpano"
+    desc: 基于 `krpano` 的 React[组件库](#/krpano)
+---
+
+## 如何使用
+
+```bash
+# 安装
+yarn add @dage/pc-components --registry http://192.168.20.245:4873/
+
+# 更新
+yarn upgrade @dage/pc-components --registry http://192.168.20.245:4873/
+```
+
+## 使用 CLI 创建后台模板
+
+```bash
+# 安装
+npm i -g @dage/backend-cli --registry http://192.168.20.245:4873/
+
+# 查看版本号
+create-dage-backend --version
+
+# 创建项目
+# 初始化时优先使用 pnpm -> yarn -> npm
+create-dage-backend packageName
+
+# 不需要角色管理模块
+create-dage-backend packageName --no-role
+```

+ 8 - 2
packages/docs/docs/log/BACKEND-CLI_CHANGELOG.md

@@ -1,5 +1,11 @@
 # @dage/backend-cli
 
+## 2.1.0
+
+### Minor Changes
+
+- 新增角色管理
+
 ## 1.2.6
 
 ### Patch Changes
@@ -17,8 +23,8 @@
 
 ### Patch Changes
 
-- - 修改密码-确认密码新增必填符号
-  - request meta 新增 showError
+- 修改密码-确认密码新增必填符号
+- request meta 新增 showError
 
 ## 1.2.1
 

+ 200 - 13
pnpm-lock.yaml

@@ -86,6 +86,9 @@ importers:
       '@types/jest':
         specifier: ^27.5.2
         version: 27.5.2
+      '@types/lodash':
+        specifier: ^4.14.197
+        version: 4.17.16
       '@types/node':
         specifier: ^16.18.126
         version: 16.18.126
@@ -167,6 +170,9 @@ importers:
       jest-watch-typeahead:
         specifier: ^1.0.0
         version: 1.0.0(jest@27.4.3(node-notifier@8.0.2))
+      lodash:
+        specifier: ^4.17.21
+        version: 4.17.21
       mini-css-extract-plugin:
         specifier: ^2.4.5
         version: 2.4.5(webpack@5.64.4)
@@ -223,7 +229,7 @@ importers:
         version: 4.0.0
       sass-loader:
         specifier: ^12.3.0
-        version: 12.3.0(sass@1.63.6)(webpack@5.64.4)
+        version: 12.3.0(sass@1.86.0)(webpack@5.64.4)
       semver:
         specifier: ^7.3.5
         version: 7.7.1
@@ -257,6 +263,10 @@ importers:
       workbox-webpack-plugin:
         specifier: ^6.4.1
         version: 6.4.1(@types/babel__core@7.20.5)(webpack@5.64.4)
+    devDependencies:
+      sass:
+        specifier: ^1.86.0
+        version: 1.86.0
 
   packages/docs:
     dependencies:
@@ -1705,6 +1715,94 @@ packages:
     resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
     engines: {node: '>= 8'}
 
+  '@parcel/watcher-android-arm64@2.5.1':
+    resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [android]
+
+  '@parcel/watcher-darwin-arm64@2.5.1':
+    resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@parcel/watcher-darwin-x64@2.5.1':
+    resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@parcel/watcher-freebsd-x64@2.5.1':
+    resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@parcel/watcher-linux-arm-glibc@2.5.1':
+    resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm]
+    os: [linux]
+    libc: [glibc]
+
+  '@parcel/watcher-linux-arm-musl@2.5.1':
+    resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm]
+    os: [linux]
+    libc: [musl]
+
+  '@parcel/watcher-linux-arm64-glibc@2.5.1':
+    resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  '@parcel/watcher-linux-arm64-musl@2.5.1':
+    resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  '@parcel/watcher-linux-x64-glibc@2.5.1':
+    resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  '@parcel/watcher-linux-x64-musl@2.5.1':
+    resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  '@parcel/watcher-win32-arm64@2.5.1':
+    resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@parcel/watcher-win32-ia32@2.5.1':
+    resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@parcel/watcher-win32-x64@2.5.1':
+    resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [win32]
+
+  '@parcel/watcher@2.5.1':
+    resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
+    engines: {node: '>= 10.0.0'}
+
   '@pmmmwh/react-refresh-webpack-plugin@0.5.3':
     resolution: {integrity: sha512-OoTnFb8XEYaOuMNhVDsLRnAO6MCYHNs1g6d8pBcHhDFsi1P3lPbq/IklwtbAx9cG0W4J9KswxZtwGnejrnxp+g==}
     engines: {node: '>= 10.13'}
@@ -3272,6 +3370,10 @@ packages:
     resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
     engines: {node: '>= 8.10.0'}
 
+  chokidar@4.0.3:
+    resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
+    engines: {node: '>= 14.16.0'}
+
   chownr@1.1.4:
     resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
 
@@ -3940,6 +4042,11 @@ packages:
     resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
     engines: {node: '>=8'}
 
+  detect-libc@1.0.3:
+    resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
+    engines: {node: '>=0.10'}
+    hasBin: true
+
   detect-newline@3.1.0:
     resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
     engines: {node: '>=8'}
@@ -5418,8 +5525,8 @@ packages:
   immer@9.0.21:
     resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
 
-  immutable@4.3.7:
-    resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==}
+  immutable@5.1.1:
+    resolution: {integrity: sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==}
 
   import-cwd@2.1.0:
     resolution: {integrity: sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg==}
@@ -6981,6 +7088,9 @@ packages:
   no-case@3.0.4:
     resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
 
+  node-addon-api@7.1.1:
+    resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+
   node-fetch-npm@2.0.4:
     resolution: {integrity: sha512-iOuIQDWDyjhv9qSDrj9aq/klt6F9z1p2otB3AV7v3zBDcL/x+OfGsvGQZZCcMZbUf4Ujw1xGNQkjvGnVT22cKg==}
     engines: {node: '>=4'}
@@ -8679,6 +8789,10 @@ packages:
     resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
     engines: {node: '>=8.10.0'}
 
+  readdirp@4.1.2:
+    resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
+    engines: {node: '>= 14.18.0'}
+
   recursive-readdir@2.2.3:
     resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==}
     engines: {node: '>=6.0.0'}
@@ -9046,8 +9160,8 @@ packages:
       sass:
         optional: true
 
-  sass@1.63.6:
-    resolution: {integrity: sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==}
+  sass@1.86.0:
+    resolution: {integrity: sha512-zV8vGUld/+mP4KbMLJMX7TyGCuUp7hnkOScgCMsWuHtns8CWBoz+vmEhoGMXsaJrbUP8gj+F1dLvVe79sK8UdA==}
     engines: {node: '>=14.0.0'}
     hasBin: true
 
@@ -12548,6 +12662,67 @@ snapshots:
       '@nodelib/fs.scandir': 2.1.5
       fastq: 1.15.0
 
+  '@parcel/watcher-android-arm64@2.5.1':
+    optional: true
+
+  '@parcel/watcher-darwin-arm64@2.5.1':
+    optional: true
+
+  '@parcel/watcher-darwin-x64@2.5.1':
+    optional: true
+
+  '@parcel/watcher-freebsd-x64@2.5.1':
+    optional: true
+
+  '@parcel/watcher-linux-arm-glibc@2.5.1':
+    optional: true
+
+  '@parcel/watcher-linux-arm-musl@2.5.1':
+    optional: true
+
+  '@parcel/watcher-linux-arm64-glibc@2.5.1':
+    optional: true
+
+  '@parcel/watcher-linux-arm64-musl@2.5.1':
+    optional: true
+
+  '@parcel/watcher-linux-x64-glibc@2.5.1':
+    optional: true
+
+  '@parcel/watcher-linux-x64-musl@2.5.1':
+    optional: true
+
+  '@parcel/watcher-win32-arm64@2.5.1':
+    optional: true
+
+  '@parcel/watcher-win32-ia32@2.5.1':
+    optional: true
+
+  '@parcel/watcher-win32-x64@2.5.1':
+    optional: true
+
+  '@parcel/watcher@2.5.1':
+    dependencies:
+      detect-libc: 1.0.3
+      is-glob: 4.0.3
+      micromatch: 4.0.8
+      node-addon-api: 7.1.1
+    optionalDependencies:
+      '@parcel/watcher-android-arm64': 2.5.1
+      '@parcel/watcher-darwin-arm64': 2.5.1
+      '@parcel/watcher-darwin-x64': 2.5.1
+      '@parcel/watcher-freebsd-x64': 2.5.1
+      '@parcel/watcher-linux-arm-glibc': 2.5.1
+      '@parcel/watcher-linux-arm-musl': 2.5.1
+      '@parcel/watcher-linux-arm64-glibc': 2.5.1
+      '@parcel/watcher-linux-arm64-musl': 2.5.1
+      '@parcel/watcher-linux-x64-glibc': 2.5.1
+      '@parcel/watcher-linux-x64-musl': 2.5.1
+      '@parcel/watcher-win32-arm64': 2.5.1
+      '@parcel/watcher-win32-ia32': 2.5.1
+      '@parcel/watcher-win32-x64': 2.5.1
+    optional: true
+
   '@pmmmwh/react-refresh-webpack-plugin@0.5.3(react-refresh@0.11.0)(type-fest@0.21.3)(webpack-dev-server@4.6.0(webpack@5.64.4))(webpack@5.64.4)':
     dependencies:
       ansi-html-community: 0.0.8
@@ -14860,6 +15035,10 @@ snapshots:
     optionalDependencies:
       fsevents: 2.3.3
 
+  chokidar@4.0.3:
+    dependencies:
+      readdirp: 4.1.2
+
   chownr@1.1.4: {}
 
   chrome-trace-event@1.0.3: {}
@@ -15577,6 +15756,9 @@ snapshots:
 
   detect-indent@6.1.0: {}
 
+  detect-libc@1.0.3:
+    optional: true
+
   detect-newline@3.1.0: {}
 
   detect-node@2.1.0: {}
@@ -17573,8 +17755,7 @@ snapshots:
 
   immer@9.0.21: {}
 
-  immutable@4.3.7:
-    optional: true
+  immutable@5.1.1: {}
 
   import-cwd@2.1.0:
     dependencies:
@@ -19851,6 +20032,9 @@ snapshots:
       lower-case: 2.0.2
       tslib: 2.8.1
 
+  node-addon-api@7.1.1:
+    optional: true
+
   node-fetch-npm@2.0.4:
     dependencies:
       encoding: 0.1.13
@@ -21926,6 +22110,8 @@ snapshots:
     dependencies:
       picomatch: 2.3.1
 
+  readdirp@4.1.2: {}
+
   recursive-readdir@2.2.3:
     dependencies:
       minimatch: 3.1.2
@@ -22317,20 +22503,21 @@ snapshots:
 
   sanitize.css@13.0.0: {}
 
-  sass-loader@12.3.0(sass@1.63.6)(webpack@5.64.4):
+  sass-loader@12.3.0(sass@1.86.0)(webpack@5.64.4):
     dependencies:
       klona: 2.0.6
       neo-async: 2.6.2
       webpack: 5.64.4
     optionalDependencies:
-      sass: 1.63.6
+      sass: 1.86.0
 
-  sass@1.63.6:
+  sass@1.86.0:
     dependencies:
-      chokidar: 3.5.3
-      immutable: 4.3.7
+      chokidar: 4.0.3
+      immutable: 5.1.1
       source-map-js: 1.2.1
-    optional: true
+    optionalDependencies:
+      '@parcel/watcher': 2.5.1
 
   sax@1.2.4: {}