chenlei 9 mesi fa
commit
0740304b96
68 ha cambiato i file con 16668 aggiunte e 0 eliminazioni
  1. 23 0
      .gitignore
  2. 2 0
      .npmrc
  3. 4 0
      .vscode/settings.json
  4. 74 0
      CHANGELOG.md
  5. 1 0
      README.md
  6. 104 0
      config/env.js
  7. 66 0
      config/getHttpsConfig.js
  8. 29 0
      config/jest/babelTransform.js
  9. 14 0
      config/jest/cssTransform.js
  10. 40 0
      config/jest/fileTransform.js
  11. 134 0
      config/modules.js
  12. 77 0
      config/paths.js
  13. 762 0
      config/webpack.config.js
  14. 9 0
      config/webpack/persistentCache/createEnvironmentHash.js
  15. 127 0
      config/webpackDevServer.config.js
  16. 160 0
      package.json
  17. 12604 0
      pnpm-lock.yaml
  18. BIN
      public/favicon.ico
  19. BIN
      public/fonts/SOURCEHANSERIFCN-BOLD.OTF
  20. BIN
      public/fonts/SOURCEHANSERIFCN-REGULAR.OTF
  21. 23 0
      public/index.html
  22. 217 0
      scripts/build.js
  23. 154 0
      scripts/start.js
  24. 52 0
      scripts/test.js
  25. 93 0
      src/App.scss
  26. 40 0
      src/App.tsx
  27. 17 0
      src/api/index.ts
  28. 8 0
      src/api/log.ts
  29. 23 0
      src/api/user.ts
  30. BIN
      src/assets/images/logo.png
  31. 22 0
      src/components/FormPageFooter/index.scss
  32. 52 0
      src/components/FormPageFooter/index.tsx
  33. 26 0
      src/components/NotFound/index.tsx
  34. 1 0
      src/components/index.ts
  35. 64 0
      src/configure.ts
  36. 9 0
      src/css.d.ts
  37. 7 0
      src/img.d.ts
  38. 19 0
      src/index.tsx
  39. 11 0
      src/pages/Assessment/Index/index.module.scss
  40. 59 0
      src/pages/Assessment/Index/index.tsx
  41. 98 0
      src/pages/Layout/components/Header/components/ResetPassword.tsx
  42. 37 0
      src/pages/Layout/components/Header/index.module.scss
  43. 113 0
      src/pages/Layout/components/Header/index.tsx
  44. 88 0
      src/pages/Layout/components/Menu/index.tsx
  45. 2 0
      src/pages/Layout/components/index.ts
  46. 78 0
      src/pages/Layout/index.scss
  47. 129 0
      src/pages/Layout/index.tsx
  48. 44 0
      src/pages/Layout/utils.ts
  49. 141 0
      src/pages/Log/index.tsx
  50. BIN
      src/pages/Login/images/bg.jpg
  51. BIN
      src/pages/Login/images/icon_account.png
  52. BIN
      src/pages/Login/images/icon_password.png
  53. 115 0
      src/pages/Login/index.scss
  54. 94 0
      src/pages/Login/index.tsx
  55. 19 0
      src/pages/User/components/UserAdd/index.module.scss
  56. 141 0
      src/pages/User/components/UserAdd/index.tsx
  57. 227 0
      src/pages/User/index.tsx
  58. 40 0
      src/router/index.tsx
  59. 16 0
      src/router/types.ts
  60. 20 0
      src/store/index.ts
  61. 22 0
      src/store/reducer/base.ts
  62. 11 0
      src/store/reducer/index.ts
  63. 6 0
      src/theme.scss
  64. 30 0
      src/types/index.ts
  65. 7 0
      src/types/log.ts
  66. 31 0
      src/types/user.ts
  67. 9 0
      src/utils/index.ts
  68. 23 0
      tsconfig.json

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+node_modules
+npm-debug.log*
+yarn-error.log
+yarn.lock
+package-lock.json
+
+# production
+/es
+docs-dist
+
+# misc
+.DS_Store
+
+# ide
+/.idea
+
+dist
+dist-node
+build
+node_modules

+ 2 - 0
.npmrc

@@ -0,0 +1,2 @@
+registry=https://registry.npmmirror.com/
+@dage:registry=http://192.168.20.245:4873/

+ 4 - 0
.vscode/settings.json

@@ -0,0 +1,4 @@
+{
+  "editor.defaultFormatter": "esbenp.prettier-vscode",
+  "editor.formatOnSave": true
+}

+ 74 - 0
CHANGELOG.md

@@ -0,0 +1,74 @@
+# @dage/backend-template
+
+## 1.0.11
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/pc-components@1.3.0
+
+## 1.0.10
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/pc-components@1.2.10
+
+## 1.0.9
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/pc-components@1.2.9
+
+## 1.0.8
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/pc-components@1.2.8
+
+## 1.0.7
+
+### Patch Changes
+
+- Updated dependencies
+- Updated dependencies
+  - @dage/service@1.0.3
+  - @dage/pc-components@1.2.7
+
+## 1.0.6
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/pc-components@1.2.6
+
+## 1.0.5
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/service@1.0.2
+  - @dage/pc-components@1.2.5
+
+## 1.0.4
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/pc-components@1.2.4
+
+## 1.0.3
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/pc-components@1.2.3
+
+## 1.0.2
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/pc-components@1.2.2

+ 1 - 0
README.md

@@ -0,0 +1 @@
+# ZHS2409030-1	2024首都大运河博物馆数字化

+ 104 - 0
config/env.js

@@ -0,0 +1,104 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const paths = require('./paths');
+
+// Make sure that including paths.js after env.js will read .env variables.
+delete require.cache[require.resolve('./paths')];
+
+const NODE_ENV = process.env.NODE_ENV;
+if (!NODE_ENV) {
+  throw new Error(
+    'The NODE_ENV environment variable is required but was not specified.'
+  );
+}
+
+// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
+const dotenvFiles = [
+  `${paths.dotenv}.${NODE_ENV}.local`,
+  // Don't include `.env.local` for `test` environment
+  // since normally you expect tests to produce the same
+  // results for everyone
+  NODE_ENV !== 'test' && `${paths.dotenv}.local`,
+  `${paths.dotenv}.${NODE_ENV}`,
+  paths.dotenv,
+].filter(Boolean);
+
+// Load environment variables from .env* files. Suppress warnings using silent
+// if this file is missing. dotenv will never modify any environment variables
+// that have already been set.  Variable expansion is supported in .env files.
+// https://github.com/motdotla/dotenv
+// https://github.com/motdotla/dotenv-expand
+dotenvFiles.forEach(dotenvFile => {
+  if (fs.existsSync(dotenvFile)) {
+    require('dotenv-expand')(
+      require('dotenv').config({
+        path: dotenvFile,
+      })
+    );
+  }
+});
+
+// We support resolving modules according to `NODE_PATH`.
+// This lets you use absolute paths in imports inside large monorepos:
+// https://github.com/facebook/create-react-app/issues/253.
+// It works similar to `NODE_PATH` in Node itself:
+// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
+// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
+// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
+// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
+// We also resolve them to make sure all tools using them work consistently.
+const appDirectory = fs.realpathSync(process.cwd());
+process.env.NODE_PATH = (process.env.NODE_PATH || '')
+  .split(path.delimiter)
+  .filter(folder => folder && !path.isAbsolute(folder))
+  .map(folder => path.resolve(appDirectory, folder))
+  .join(path.delimiter);
+
+// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
+// injected into the application via DefinePlugin in webpack configuration.
+const REACT_APP = /^REACT_APP_/i;
+
+function getClientEnvironment(publicUrl) {
+  const raw = Object.keys(process.env)
+    .filter(key => REACT_APP.test(key))
+    .reduce(
+      (env, key) => {
+        env[key] = process.env[key];
+        return env;
+      },
+      {
+        // Useful for determining whether we’re running in production mode.
+        // Most importantly, it switches React into the correct mode.
+        NODE_ENV: process.env.NODE_ENV || 'development',
+        // Useful for resolving the correct path to static assets in `public`.
+        // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
+        // This should only be used as an escape hatch. Normally you would put
+        // images into the `src` and `import` them in code to get their paths.
+        PUBLIC_URL: publicUrl,
+        // We support configuring the sockjs pathname during development.
+        // These settings let a developer run multiple simultaneous projects.
+        // They are used as the connection `hostname`, `pathname` and `port`
+        // in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
+        // and `sockPort` options in webpack-dev-server.
+        WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
+        WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
+        WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
+        // Whether or not react-refresh is enabled.
+        // It is defined here so it is available in the webpackHotDevClient.
+        FAST_REFRESH: process.env.FAST_REFRESH !== 'false',
+      }
+    );
+  // Stringify all values so we can feed into webpack DefinePlugin
+  const stringified = {
+    'process.env': Object.keys(raw).reduce((env, key) => {
+      env[key] = JSON.stringify(raw[key]);
+      return env;
+    }, {}),
+  };
+
+  return { raw, stringified };
+}
+
+module.exports = getClientEnvironment;

+ 66 - 0
config/getHttpsConfig.js

@@ -0,0 +1,66 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const chalk = require('react-dev-utils/chalk');
+const paths = require('./paths');
+
+// Ensure the certificate and key provided are valid and if not
+// throw an easy to debug error
+function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
+  let encrypted;
+  try {
+    // publicEncrypt will throw an error with an invalid cert
+    encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
+  } catch (err) {
+    throw new Error(
+      `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
+    );
+  }
+
+  try {
+    // privateDecrypt will throw an error with an invalid key
+    crypto.privateDecrypt(key, encrypted);
+  } catch (err) {
+    throw new Error(
+      `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
+        err.message
+      }`
+    );
+  }
+}
+
+// Read file and throw an error if it doesn't exist
+function readEnvFile(file, type) {
+  if (!fs.existsSync(file)) {
+    throw new Error(
+      `You specified ${chalk.cyan(
+        type
+      )} in your env, but the file "${chalk.yellow(file)}" can't be found.`
+    );
+  }
+  return fs.readFileSync(file);
+}
+
+// Get the https config
+// Return cert files if provided in env, otherwise just true or false
+function getHttpsConfig() {
+  const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
+  const isHttps = HTTPS === 'true';
+
+  if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
+    const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
+    const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
+    const config = {
+      cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
+      key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
+    };
+
+    validateKeyAndCerts({ ...config, keyFile, crtFile });
+    return config;
+  }
+  return isHttps;
+}
+
+module.exports = getHttpsConfig;

+ 29 - 0
config/jest/babelTransform.js

@@ -0,0 +1,29 @@
+'use strict';
+
+const babelJest = require('babel-jest').default;
+
+const hasJsxRuntime = (() => {
+  if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
+    return false;
+  }
+
+  try {
+    require.resolve('react/jsx-runtime');
+    return true;
+  } catch (e) {
+    return false;
+  }
+})();
+
+module.exports = babelJest.createTransformer({
+  presets: [
+    [
+      require.resolve('babel-preset-react-app'),
+      {
+        runtime: hasJsxRuntime ? 'automatic' : 'classic',
+      },
+    ],
+  ],
+  babelrc: false,
+  configFile: false,
+});

+ 14 - 0
config/jest/cssTransform.js

@@ -0,0 +1,14 @@
+'use strict';
+
+// This is a custom Jest transformer turning style imports into empty objects.
+// http://facebook.github.io/jest/docs/en/webpack.html
+
+module.exports = {
+  process() {
+    return 'module.exports = {};';
+  },
+  getCacheKey() {
+    // The output is always the same.
+    return 'cssTransform';
+  },
+};

+ 40 - 0
config/jest/fileTransform.js

@@ -0,0 +1,40 @@
+'use strict';
+
+const path = require('path');
+const camelcase = require('camelcase');
+
+// This is a custom Jest transformer turning file imports into filenames.
+// http://facebook.github.io/jest/docs/en/webpack.html
+
+module.exports = {
+  process(src, filename) {
+    const assetFilename = JSON.stringify(path.basename(filename));
+
+    if (filename.match(/\.svg$/)) {
+      // Based on how SVGR generates a component name:
+      // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
+      const pascalCaseFilename = camelcase(path.parse(filename).name, {
+        pascalCase: true,
+      });
+      const componentName = `Svg${pascalCaseFilename}`;
+      return `const React = require('react');
+      module.exports = {
+        __esModule: true,
+        default: ${assetFilename},
+        ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
+          return {
+            $$typeof: Symbol.for('react.element'),
+            type: 'svg',
+            ref: ref,
+            key: null,
+            props: Object.assign({}, props, {
+              children: ${assetFilename}
+            })
+          };
+        }),
+      };`;
+    }
+
+    return `module.exports = ${assetFilename};`;
+  },
+};

+ 134 - 0
config/modules.js

@@ -0,0 +1,134 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const paths = require('./paths');
+const chalk = require('react-dev-utils/chalk');
+const resolve = require('resolve');
+
+/**
+ * Get additional module paths based on the baseUrl of a compilerOptions object.
+ *
+ * @param {Object} options
+ */
+function getAdditionalModulePaths(options = {}) {
+  const baseUrl = options.baseUrl;
+
+  if (!baseUrl) {
+    return '';
+  }
+
+  const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
+
+  // We don't need to do anything if `baseUrl` is set to `node_modules`. This is
+  // the default behavior.
+  if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
+    return null;
+  }
+
+  // Allow the user set the `baseUrl` to `appSrc`.
+  if (path.relative(paths.appSrc, baseUrlResolved) === '') {
+    return [paths.appSrc];
+  }
+
+  // If the path is equal to the root directory we ignore it here.
+  // We don't want to allow importing from the root directly as source files are
+  // not transpiled outside of `src`. We do allow importing them with the
+  // absolute path (e.g. `src/Components/Button.js`) but we set that up with
+  // an alias.
+  if (path.relative(paths.appPath, baseUrlResolved) === '') {
+    return null;
+  }
+
+  // Otherwise, throw an error.
+  throw new Error(
+    chalk.red.bold(
+      "Your project's `baseUrl` can only be set to `src` or `node_modules`." +
+        ' Create React App does not support other values at this time.'
+    )
+  );
+}
+
+/**
+ * Get webpack aliases based on the baseUrl of a compilerOptions object.
+ *
+ * @param {*} options
+ */
+function getWebpackAliases(options = {}) {
+  const baseUrl = options.baseUrl;
+
+  if (!baseUrl) {
+    return {};
+  }
+
+  const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
+
+  if (path.relative(paths.appPath, baseUrlResolved) === '') {
+    return {
+      src: paths.appSrc,
+    };
+  }
+}
+
+/**
+ * Get jest aliases based on the baseUrl of a compilerOptions object.
+ *
+ * @param {*} options
+ */
+function getJestAliases(options = {}) {
+  const baseUrl = options.baseUrl;
+
+  if (!baseUrl) {
+    return {};
+  }
+
+  const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
+
+  if (path.relative(paths.appPath, baseUrlResolved) === '') {
+    return {
+      '^src/(.*)$': '<rootDir>/src/$1',
+    };
+  }
+}
+
+function getModules() {
+  // Check if TypeScript is setup
+  const hasTsConfig = fs.existsSync(paths.appTsConfig);
+  const hasJsConfig = fs.existsSync(paths.appJsConfig);
+
+  if (hasTsConfig && hasJsConfig) {
+    throw new Error(
+      'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
+    );
+  }
+
+  let config;
+
+  // If there's a tsconfig.json we assume it's a
+  // TypeScript project and set up the config
+  // based on tsconfig.json
+  if (hasTsConfig) {
+    const ts = require(resolve.sync('typescript', {
+      basedir: paths.appNodeModules,
+    }));
+    config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
+    // Otherwise we'll check if there is jsconfig.json
+    // for non TS projects.
+  } else if (hasJsConfig) {
+    config = require(paths.appJsConfig);
+  }
+
+  config = config || {};
+  const options = config.compilerOptions || {};
+
+  const additionalModulePaths = getAdditionalModulePaths(options);
+
+  return {
+    additionalModulePaths: additionalModulePaths,
+    webpackAliases: getWebpackAliases(options),
+    jestAliases: getJestAliases(options),
+    hasTsConfig,
+  };
+}
+
+module.exports = getModules();

+ 77 - 0
config/paths.js

@@ -0,0 +1,77 @@
+'use strict';
+
+const path = require('path');
+const fs = require('fs');
+const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
+
+// Make sure any symlinks in the project folder are resolved:
+// https://github.com/facebook/create-react-app/issues/637
+const appDirectory = fs.realpathSync(process.cwd());
+const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
+
+// We use `PUBLIC_URL` environment variable or "homepage" field to infer
+// "public path" at which the app is served.
+// webpack needs to know it to put the right <script> hrefs into HTML even in
+// single-page apps that may serve index.html for nested URLs like /todos/42.
+// We can't use a relative path in HTML because we don't want to load something
+// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
+const publicUrlOrPath = getPublicUrlOrPath(
+  process.env.NODE_ENV === 'development',
+  require(resolveApp('package.json')).homepage,
+  process.env.PUBLIC_URL
+);
+
+const buildPath = process.env.BUILD_PATH || 'build';
+
+const moduleFileExtensions = [
+  'web.mjs',
+  'mjs',
+  'web.js',
+  'js',
+  'web.ts',
+  'ts',
+  'web.tsx',
+  'tsx',
+  'json',
+  'web.jsx',
+  'jsx',
+];
+
+// Resolve file paths in the same order as webpack
+const resolveModule = (resolveFn, filePath) => {
+  const extension = moduleFileExtensions.find(extension =>
+    fs.existsSync(resolveFn(`${filePath}.${extension}`))
+  );
+
+  if (extension) {
+    return resolveFn(`${filePath}.${extension}`);
+  }
+
+  return resolveFn(`${filePath}.js`);
+};
+
+// config after eject: we're in ./config/
+module.exports = {
+  dotenv: resolveApp('.env'),
+  appPath: resolveApp('.'),
+  appBuild: resolveApp(buildPath),
+  appPublic: resolveApp('public'),
+  appHtml: resolveApp('public/index.html'),
+  appIndexJs: resolveModule(resolveApp, 'src/index'),
+  appPackageJson: resolveApp('package.json'),
+  appSrc: resolveApp('src'),
+  appTsConfig: resolveApp('tsconfig.json'),
+  appJsConfig: resolveApp('jsconfig.json'),
+  yarnLockFile: resolveApp('yarn.lock'),
+  testsSetup: resolveModule(resolveApp, 'src/setupTests'),
+  proxySetup: resolveApp('src/setupProxy.js'),
+  appNodeModules: resolveApp('node_modules'),
+  appWebpackCache: resolveApp('node_modules/.cache'),
+  appTsBuildInfoFile: resolveApp('node_modules/.cache/tsconfig.tsbuildinfo'),
+  swSrc: resolveModule(resolveApp, 'src/service-worker'),
+  publicUrlOrPath,
+};
+
+
+
+module.exports.moduleFileExtensions = moduleFileExtensions;

+ 762 - 0
config/webpack.config.js

@@ -0,0 +1,762 @@
+"use strict";
+
+const fs = require("fs");
+const path = require("path");
+const webpack = require("webpack");
+const resolve = require("resolve");
+const componentImportOptions = require("@dage/pc-components/babel-import-config");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const CaseSensitivePathsPlugin = require("case-sensitive-paths-webpack-plugin");
+const InlineChunkHtmlPlugin = require("react-dev-utils/InlineChunkHtmlPlugin");
+const TerserPlugin = require("terser-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
+const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
+const InterpolateHtmlPlugin = require("react-dev-utils/InterpolateHtmlPlugin");
+const WorkboxWebpackPlugin = require("workbox-webpack-plugin");
+const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
+const getCSSModuleLocalIdent = require("react-dev-utils/getCSSModuleLocalIdent");
+const ESLintPlugin = require("eslint-webpack-plugin");
+const paths = require("./paths");
+const modules = require("./modules");
+const getClientEnvironment = require("./env");
+const ModuleNotFoundPlugin = require("react-dev-utils/ModuleNotFoundPlugin");
+const ForkTsCheckerWebpackPlugin =
+  process.env.TSC_COMPILE_ON_ERROR === "true"
+    ? require("react-dev-utils/ForkTsCheckerWarningWebpackPlugin")
+    : require("react-dev-utils/ForkTsCheckerWebpackPlugin");
+const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
+
+const createEnvironmentHash = require("./webpack/persistentCache/createEnvironmentHash");
+
+// Source maps are resource heavy and can cause out of memory issue for large source files.
+const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
+
+const reactRefreshRuntimeEntry = require.resolve("react-refresh/runtime");
+const reactRefreshWebpackPluginRuntimeEntry = require.resolve(
+  "@pmmmwh/react-refresh-webpack-plugin"
+);
+const babelRuntimeEntry = require.resolve("babel-preset-react-app");
+const babelRuntimeEntryHelpers = require.resolve(
+  "@babel/runtime/helpers/esm/assertThisInitialized",
+  { paths: [babelRuntimeEntry] }
+);
+const babelRuntimeRegenerator = require.resolve("@babel/runtime/regenerator", {
+  paths: [babelRuntimeEntry],
+});
+
+// Some apps do not need the benefits of saving a web request, so not inlining the chunk
+// makes for a smoother build process.
+const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== "false";
+
+const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === "true";
+const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === "true";
+
+const imageInlineSizeLimit = parseInt(
+  process.env.IMAGE_INLINE_SIZE_LIMIT || "10000"
+);
+
+// Check if TypeScript is setup
+const useTypeScript = fs.existsSync(paths.appTsConfig);
+
+// Check if Tailwind config exists
+const useTailwind = fs.existsSync(
+  path.join(paths.appPath, "tailwind.config.js")
+);
+
+// Get the path to the uncompiled service worker (if it exists).
+const swSrc = paths.swSrc;
+
+// style files regexes
+const cssRegex = /\.css$/;
+const cssModuleRegex = /\.module\.css$/;
+const sassRegex = /\.(scss|sass)$/;
+const sassModuleRegex = /\.module\.(scss|sass)$/;
+
+const hasJsxRuntime = (() => {
+  if (process.env.DISABLE_NEW_JSX_TRANSFORM === "true") {
+    return false;
+  }
+
+  try {
+    require.resolve("react/jsx-runtime");
+    return true;
+  } catch (e) {
+    return false;
+  }
+})();
+
+// This is the production and development configuration.
+// It is focused on developer experience, fast rebuilds, and a minimal bundle.
+module.exports = function (webpackEnv) {
+  const isEnvDevelopment = webpackEnv === "development";
+  const isEnvProduction = webpackEnv === "production";
+
+  // Variable used for enabling profiling in Production
+  // passed into alias object. Uses a flag if passed into the build command
+  const isEnvProductionProfile =
+    isEnvProduction && process.argv.includes("--profile");
+
+  // We will provide `paths.publicUrlOrPath` to our app
+  // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
+  // Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
+  // Get environment variables to inject into our app.
+  const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
+
+  const shouldUseReactRefresh = env.raw.FAST_REFRESH;
+
+  // common function to get style loaders
+  const getStyleLoaders = (cssOptions, preProcessor) => {
+    const loaders = [
+      isEnvDevelopment && require.resolve("style-loader"),
+      isEnvProduction && {
+        loader: MiniCssExtractPlugin.loader,
+        // css is located in `static/css`, use '../../' to locate index.html folder
+        // in production `paths.publicUrlOrPath` can be a relative path
+        options: paths.publicUrlOrPath.startsWith(".")
+          ? { publicPath: "../../" }
+          : {},
+      },
+      {
+        loader: require.resolve("css-loader"),
+        options: cssOptions,
+      },
+      {
+        // Options for PostCSS as we reference these options twice
+        // Adds vendor prefixing based on your specified browser support in
+        // package.json
+        loader: require.resolve("postcss-loader"),
+        options: {
+          postcssOptions: {
+            // Necessary for external CSS imports to work
+            // https://github.com/facebook/create-react-app/issues/2677
+            ident: "postcss",
+            config: false,
+            plugins: !useTailwind
+              ? [
+                  "postcss-flexbugs-fixes",
+                  [
+                    "postcss-preset-env",
+                    {
+                      autoprefixer: {
+                        flexbox: "no-2009",
+                      },
+                      stage: 3,
+                    },
+                  ],
+                  // Adds PostCSS Normalize as the reset css with default options,
+                  // so that it honors browserslist config in package.json
+                  // which in turn let's users customize the target behavior as per their needs.
+                  "postcss-normalize",
+                ]
+              : [
+                  "tailwindcss",
+                  "postcss-flexbugs-fixes",
+                  [
+                    "postcss-preset-env",
+                    {
+                      autoprefixer: {
+                        flexbox: "no-2009",
+                      },
+                      stage: 3,
+                    },
+                  ],
+                ],
+          },
+          sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
+        },
+      },
+    ].filter(Boolean);
+    if (preProcessor) {
+      loaders.push(
+        {
+          loader: require.resolve("resolve-url-loader"),
+          options: {
+            sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
+            root: paths.appSrc,
+          },
+        },
+        {
+          loader: require.resolve(preProcessor),
+          options: {
+            sourceMap: true,
+          },
+        }
+      );
+    }
+    return loaders;
+  };
+
+  return {
+    target: ["browserslist"],
+    // Webpack noise constrained to errors and warnings
+    stats: "errors-warnings",
+    mode: isEnvProduction ? "production" : isEnvDevelopment && "development",
+    // Stop compilation early in production
+    bail: isEnvProduction,
+    devtool: isEnvProduction
+      ? shouldUseSourceMap
+        ? "source-map"
+        : false
+      : isEnvDevelopment && "cheap-module-source-map",
+    // These are the "entry points" to our application.
+    // This means they will be the "root" imports that are included in JS bundle.
+    entry: paths.appIndexJs,
+    output: {
+      // The build folder.
+      path: paths.appBuild,
+      // Add /* filename */ comments to generated require()s in the output.
+      pathinfo: isEnvDevelopment,
+      // There will be one main bundle, and one file per asynchronous chunk.
+      // In development, it does not produce real files.
+      filename: isEnvProduction
+        ? "static/js/[name].[contenthash:8].js"
+        : isEnvDevelopment && "static/js/bundle.js",
+      // There are also additional JS chunk files if you use code splitting.
+      chunkFilename: isEnvProduction
+        ? "static/js/[name].[contenthash:8].chunk.js"
+        : isEnvDevelopment && "static/js/[name].chunk.js",
+      assetModuleFilename: "static/media/[name].[hash][ext]",
+      // webpack uses `publicPath` to determine where the app is being served from.
+      // It requires a trailing slash, or the file assets will get an incorrect path.
+      // We inferred the "public path" (such as / or /my-project) from homepage.
+      publicPath: paths.publicUrlOrPath,
+      // Point sourcemap entries to original disk location (format as URL on Windows)
+      devtoolModuleFilenameTemplate: isEnvProduction
+        ? (info) =>
+            path
+              .relative(paths.appSrc, info.absoluteResourcePath)
+              .replace(/\\/g, "/")
+        : isEnvDevelopment &&
+          ((info) =>
+            path.resolve(info.absoluteResourcePath).replace(/\\/g, "/")),
+    },
+    cache: {
+      type: "filesystem",
+      version: createEnvironmentHash(env.raw),
+      cacheDirectory: paths.appWebpackCache,
+      store: "pack",
+      buildDependencies: {
+        defaultWebpack: ["webpack/lib/"],
+        config: [__filename],
+        tsconfig: [paths.appTsConfig, paths.appJsConfig].filter((f) =>
+          fs.existsSync(f)
+        ),
+      },
+    },
+    infrastructureLogging: {
+      level: "none",
+    },
+    optimization: {
+      minimize: isEnvProduction,
+      minimizer: [
+        // This is only used in production mode
+        new TerserPlugin({
+          terserOptions: {
+            parse: {
+              // We want terser to parse ecma 8 code. However, we don't want it
+              // to apply any minification steps that turns valid ecma 5 code
+              // into invalid ecma 5 code. This is why the 'compress' and 'output'
+              // sections only apply transformations that are ecma 5 safe
+              // https://github.com/facebook/create-react-app/pull/4234
+              ecma: 8,
+            },
+            compress: {
+              ecma: 5,
+              warnings: false,
+              // Disabled because of an issue with Uglify breaking seemingly valid code:
+              // https://github.com/facebook/create-react-app/issues/2376
+              // Pending further investigation:
+              // https://github.com/mishoo/UglifyJS2/issues/2011
+              comparisons: false,
+              // Disabled because of an issue with Terser breaking valid code:
+              // https://github.com/facebook/create-react-app/issues/5250
+              // Pending further investigation:
+              // https://github.com/terser-js/terser/issues/120
+              inline: 2,
+            },
+            mangle: {
+              safari10: true,
+            },
+            // Added for profiling in devtools
+            keep_classnames: isEnvProductionProfile,
+            keep_fnames: isEnvProductionProfile,
+            output: {
+              ecma: 5,
+              comments: false,
+              // Turned on because emoji and regex is not minified properly using default
+              // https://github.com/facebook/create-react-app/issues/2488
+              ascii_only: true,
+            },
+          },
+        }),
+        // This is only used in production mode
+        new CssMinimizerPlugin(),
+      ],
+    },
+    resolve: {
+      // This allows you to set a fallback for where webpack should look for modules.
+      // We placed these paths second because we want `node_modules` to "win"
+      // if there are any conflicts. This matches Node resolution mechanism.
+      // https://github.com/facebook/create-react-app/issues/253
+      modules: ["node_modules", paths.appNodeModules].concat(
+        modules.additionalModulePaths || []
+      ),
+      // These are the reasonable defaults supported by the Node ecosystem.
+      // We also include JSX as a common component filename extension to support
+      // some tools, although we do not recommend using it, see:
+      // https://github.com/facebook/create-react-app/issues/290
+      // `web` extension prefixes have been added for better support
+      // for React Native Web.
+      extensions: paths.moduleFileExtensions
+        .map((ext) => `.${ext}`)
+        .filter((ext) => useTypeScript || !ext.includes("ts")),
+      alias: {
+        // Support React Native Web
+        // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
+        "react-native": "react-native-web",
+        // Allows for better profiling with ReactDevTools
+        ...(isEnvProductionProfile && {
+          "react-dom$": "react-dom/profiling",
+          "scheduler/tracing": "scheduler/tracing-profiling",
+        }),
+        ...(modules.webpackAliases || {}),
+        "@": path.resolve(__dirname, "../src"),
+      },
+      plugins: [
+        // Prevents users from importing files from outside of src/ (or node_modules/).
+        // This often causes confusion because we only process files within src/ with babel.
+        // To fix this, we prevent you from importing files out of src/ -- if you'd like to,
+        // please link the files into your node_modules/ and let module-resolution kick in.
+        // Make sure your source files are compiled, as they will not be processed in any way.
+        new ModuleScopePlugin(paths.appSrc, [
+          paths.appPackageJson,
+          reactRefreshRuntimeEntry,
+          reactRefreshWebpackPluginRuntimeEntry,
+          babelRuntimeEntry,
+          babelRuntimeEntryHelpers,
+          babelRuntimeRegenerator,
+        ]),
+      ],
+    },
+    module: {
+      strictExportPresence: true,
+      rules: [
+        // Handle node_modules packages that contain sourcemaps
+        shouldUseSourceMap && {
+          enforce: "pre",
+          exclude: /@babel(?:\/|\\{1,2})runtime/,
+          test: /\.(js|mjs|jsx|ts|tsx|css)$/,
+          loader: require.resolve("source-map-loader"),
+        },
+        {
+          // "oneOf" will traverse all following loaders until one will
+          // match the requirements. When no loader matches it will fall
+          // back to the "file" loader at the end of the loader list.
+          oneOf: [
+            // TODO: Merge this config once `image/avif` is in the mime-db
+            // https://github.com/jshttp/mime-db
+            {
+              test: [/\.avif$/],
+              type: "asset",
+              mimetype: "image/avif",
+              parser: {
+                dataUrlCondition: {
+                  maxSize: imageInlineSizeLimit,
+                },
+              },
+            },
+            // "url" loader works like "file" loader except that it embeds assets
+            // smaller than specified limit in bytes as data URLs to avoid requests.
+            // A missing `test` is equivalent to a match.
+            {
+              test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
+              type: "asset",
+              parser: {
+                dataUrlCondition: {
+                  maxSize: imageInlineSizeLimit,
+                },
+              },
+            },
+            {
+              test: /\.svg$/,
+              use: [
+                {
+                  loader: require.resolve("@svgr/webpack"),
+                  options: {
+                    prettier: false,
+                    svgo: false,
+                    svgoConfig: {
+                      plugins: [{ removeViewBox: false }],
+                    },
+                    titleProp: true,
+                    ref: true,
+                  },
+                },
+                {
+                  loader: require.resolve("file-loader"),
+                  options: {
+                    name: "static/media/[name].[hash].[ext]",
+                  },
+                },
+              ],
+              issuer: {
+                and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
+              },
+            },
+            // Process application JS with Babel.
+            // The preset includes JSX, Flow, TypeScript, and some ESnext features.
+            {
+              test: /\.(js|mjs|jsx|ts|tsx)$/,
+              include: paths.appSrc,
+              loader: require.resolve("babel-loader"),
+              options: {
+                customize: require.resolve(
+                  "babel-preset-react-app/webpack-overrides"
+                ),
+                presets: [
+                  [
+                    require.resolve("babel-preset-react-app"),
+                    {
+                      runtime: hasJsxRuntime ? "automatic" : "classic",
+                    },
+                  ],
+                ],
+
+                plugins: [
+                  isEnvDevelopment &&
+                    shouldUseReactRefresh &&
+                    require.resolve("react-refresh/babel"),
+                  isEnvProduction && [
+                    "babel-plugin-import",
+                    componentImportOptions,
+                  ],
+                ].filter(Boolean),
+                // This is a feature of `babel-loader` for webpack (not Babel itself).
+                // It enables caching results in ./node_modules/.cache/babel-loader/
+                // directory for faster rebuilds.
+                cacheDirectory: true,
+                // See #6846 for context on why cacheCompression is disabled
+                cacheCompression: false,
+                compact: isEnvProduction,
+              },
+            },
+            // Process any JS outside of the app with Babel.
+            // Unlike the application JS, we only compile the standard ES features.
+            {
+              test: /\.(js|mjs)$/,
+              exclude: /@babel(?:\/|\\{1,2})runtime/,
+              loader: require.resolve("babel-loader"),
+              options: {
+                babelrc: false,
+                configFile: false,
+                compact: false,
+                presets: [
+                  [
+                    require.resolve("babel-preset-react-app/dependencies"),
+                    { helpers: true },
+                  ],
+                ],
+                cacheDirectory: true,
+                // See #6846 for context on why cacheCompression is disabled
+                cacheCompression: false,
+
+                // Babel sourcemaps are needed for debugging into node_modules
+                // code.  Without the options below, debuggers like VSCode
+                // show incorrect code and set breakpoints on the wrong lines.
+                sourceMaps: shouldUseSourceMap,
+                inputSourceMap: shouldUseSourceMap,
+              },
+            },
+            // "postcss" loader applies autoprefixer to our CSS.
+            // "css" loader resolves paths in CSS and adds assets as dependencies.
+            // "style" loader turns CSS into JS modules that inject <style> tags.
+            // In production, we use MiniCSSExtractPlugin to extract that CSS
+            // to a file, but in development "style" loader enables hot editing
+            // of CSS.
+            // By default we support CSS Modules with the extension .module.css
+            {
+              test: cssRegex,
+              exclude: cssModuleRegex,
+              use: getStyleLoaders({
+                importLoaders: 1,
+                sourceMap: isEnvProduction
+                  ? shouldUseSourceMap
+                  : isEnvDevelopment,
+                modules: {
+                  mode: "icss",
+                },
+              }),
+              // Don't consider CSS imports dead code even if the
+              // containing package claims to have no side effects.
+              // Remove this when webpack adds a warning or an error for this.
+              // See https://github.com/webpack/webpack/issues/6571
+              sideEffects: true,
+            },
+            // Adds support for CSS Modules (https://github.com/css-modules/css-modules)
+            // using the extension .module.css
+            {
+              test: cssModuleRegex,
+              use: getStyleLoaders({
+                importLoaders: 1,
+                sourceMap: isEnvProduction
+                  ? shouldUseSourceMap
+                  : isEnvDevelopment,
+                modules: {
+                  mode: "local",
+                  getLocalIdent: getCSSModuleLocalIdent,
+                },
+              }),
+            },
+            // Opt-in support for SASS (using .scss or .sass extensions).
+            // By default we support SASS Modules with the
+            // extensions .module.scss or .module.sass
+            {
+              test: sassRegex,
+              exclude: sassModuleRegex,
+              use: getStyleLoaders(
+                {
+                  importLoaders: 3,
+                  sourceMap: isEnvProduction
+                    ? shouldUseSourceMap
+                    : isEnvDevelopment,
+                  modules: {
+                    mode: "icss",
+                  },
+                },
+                "sass-loader"
+              ),
+              // Don't consider CSS imports dead code even if the
+              // containing package claims to have no side effects.
+              // Remove this when webpack adds a warning or an error for this.
+              // See https://github.com/webpack/webpack/issues/6571
+              sideEffects: true,
+            },
+            // Adds support for CSS Modules, but using SASS
+            // using the extension .module.scss or .module.sass
+            {
+              test: sassModuleRegex,
+              use: getStyleLoaders(
+                {
+                  importLoaders: 3,
+                  sourceMap: isEnvProduction
+                    ? shouldUseSourceMap
+                    : isEnvDevelopment,
+                  modules: {
+                    mode: "local",
+                    getLocalIdent: getCSSModuleLocalIdent,
+                  },
+                },
+                "sass-loader"
+              ),
+            },
+            // "file" loader makes sure those assets get served by WebpackDevServer.
+            // When you `import` an asset, you get its (virtual) filename.
+            // In production, they would get copied to the `build` folder.
+            // This loader doesn't use a "test" so it will catch all modules
+            // that fall through the other loaders.
+            {
+              // Exclude `js` files to keep "css" loader working as it injects
+              // its runtime that would otherwise be processed through "file" loader.
+              // Also exclude `html` and `json` extensions so they get processed
+              // by webpacks internal loaders.
+              exclude: [/^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
+              type: "asset/resource",
+            },
+            // ** STOP ** Are you adding a new loader?
+            // Make sure to add the new loader(s) before the "file" loader.
+          ],
+        },
+      ].filter(Boolean),
+    },
+    plugins: [
+      // Generates an `index.html` file with the <script> injected.
+      new HtmlWebpackPlugin(
+        Object.assign(
+          {},
+          {
+            inject: true,
+            template: paths.appHtml,
+          },
+          isEnvProduction
+            ? {
+                minify: {
+                  removeComments: true,
+                  collapseWhitespace: true,
+                  removeRedundantAttributes: true,
+                  useShortDoctype: true,
+                  removeEmptyAttributes: true,
+                  removeStyleLinkTypeAttributes: true,
+                  keepClosingSlash: true,
+                  minifyJS: true,
+                  minifyCSS: true,
+                  minifyURLs: true,
+                },
+              }
+            : undefined
+        )
+      ),
+      // Inlines the webpack runtime script. This script is too small to warrant
+      // a network request.
+      // https://github.com/facebook/create-react-app/issues/5358
+      isEnvProduction &&
+        shouldInlineRuntimeChunk &&
+        new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
+      // Makes some environment variables available in index.html.
+      // The public URL is available as %PUBLIC_URL% in index.html, e.g.:
+      // <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
+      // It will be an empty string unless you specify "homepage"
+      // in `package.json`, in which case it will be the pathname of that URL.
+      new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
+      // This gives some necessary context to module not found errors, such as
+      // the requesting resource.
+      new ModuleNotFoundPlugin(paths.appPath),
+      // Makes some environment variables available to the JS code, for example:
+      // if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
+      // It is absolutely essential that NODE_ENV is set to production
+      // during a production build.
+      // Otherwise React will be compiled in the very slow development mode.
+      new webpack.DefinePlugin(env.stringified),
+      // Experimental hot reloading for React .
+      // https://github.com/facebook/react/tree/main/packages/react-refresh
+      isEnvDevelopment &&
+        shouldUseReactRefresh &&
+        new ReactRefreshWebpackPlugin({
+          overlay: false,
+        }),
+      // Watcher doesn't work well if you mistype casing in a path so we use
+      // a plugin that prints an error when you attempt to do this.
+      // See https://github.com/facebook/create-react-app/issues/240
+      isEnvDevelopment && new CaseSensitivePathsPlugin(),
+      isEnvProduction &&
+        new MiniCssExtractPlugin({
+          // Options similar to the same options in webpackOptions.output
+          // both options are optional
+          filename: "static/css/[name].[contenthash:8].css",
+          chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
+        }),
+      // Generate an asset manifest file with the following content:
+      // - "files" key: Mapping of all asset filenames to their corresponding
+      //   output file so that tools can pick it up without having to parse
+      //   `index.html`
+      // - "entrypoints" key: Array of files which are included in `index.html`,
+      //   can be used to reconstruct the HTML if necessary
+      new WebpackManifestPlugin({
+        fileName: "asset-manifest.json",
+        publicPath: paths.publicUrlOrPath,
+        generate: (seed, files, entrypoints) => {
+          const manifestFiles = files.reduce((manifest, file) => {
+            manifest[file.name] = file.path;
+            return manifest;
+          }, seed);
+          const entrypointFiles = entrypoints.main.filter(
+            (fileName) => !fileName.endsWith(".map")
+          );
+
+          return {
+            files: manifestFiles,
+            entrypoints: entrypointFiles,
+          };
+        },
+      }),
+      // Moment.js is an extremely popular library that bundles large locale files
+      // by default due to how webpack interprets its code. This is a practical
+      // solution that requires the user to opt into importing specific locales.
+      // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
+      // You can remove this if you don't use Moment.js:
+      new webpack.IgnorePlugin({
+        resourceRegExp: /^\.\/locale$/,
+        contextRegExp: /moment$/,
+      }),
+      // Generate a service worker script that will precache, and keep up to date,
+      // the HTML & assets that are part of the webpack build.
+      isEnvProduction &&
+        fs.existsSync(swSrc) &&
+        new WorkboxWebpackPlugin.InjectManifest({
+          swSrc,
+          dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
+          exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
+          // Bump up the default maximum size (2mb) that's precached,
+          // to make lazy-loading failure scenarios less likely.
+          // See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
+          maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
+        }),
+      // TypeScript type checking
+      useTypeScript &&
+        new ForkTsCheckerWebpackPlugin({
+          async: isEnvDevelopment,
+          typescript: {
+            typescriptPath: resolve.sync("typescript", {
+              basedir: paths.appNodeModules,
+            }),
+            configOverwrite: {
+              compilerOptions: {
+                sourceMap: isEnvProduction
+                  ? shouldUseSourceMap
+                  : isEnvDevelopment,
+                skipLibCheck: true,
+                inlineSourceMap: false,
+                declarationMap: false,
+                noEmit: true,
+                incremental: true,
+                tsBuildInfoFile: paths.appTsBuildInfoFile,
+              },
+            },
+            context: paths.appPath,
+            diagnosticOptions: {
+              syntactic: true,
+            },
+            mode: "write-references",
+            // profile: true,
+          },
+          issue: {
+            // This one is specifically to match during CI tests,
+            // as micromatch doesn't match
+            // '../cra-template-typescript/template/src/App.tsx'
+            // otherwise.
+            include: [
+              { file: "../**/src/**/*.{ts,tsx}" },
+              { file: "**/src/**/*.{ts,tsx}" },
+            ],
+            exclude: [
+              { file: "**/src/**/__tests__/**" },
+              { file: "**/src/**/?(*.){spec|test}.*" },
+              { file: "**/src/setupProxy.*" },
+              { file: "**/src/setupTests.*" },
+            ],
+          },
+          logger: {
+            infrastructure: "silent",
+          },
+        }),
+      !disableESLintPlugin &&
+        new ESLintPlugin({
+          // Plugin options
+          extensions: ["js", "mjs", "jsx", "ts", "tsx"],
+          formatter: require.resolve("react-dev-utils/eslintFormatter"),
+          eslintPath: require.resolve("eslint"),
+          failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
+          context: paths.appSrc,
+          cache: true,
+          cacheLocation: path.resolve(
+            paths.appNodeModules,
+            ".cache/.eslintcache"
+          ),
+          // ESLint class options
+          cwd: paths.appPath,
+          resolvePluginsRelativeTo: __dirname,
+          baseConfig: {
+            extends: [require.resolve("eslint-config-react-app/base")],
+            rules: {
+              ...(!hasJsxRuntime && {
+                "react/react-in-jsx-scope": "error",
+              }),
+            },
+          },
+        }),
+    ].filter(Boolean),
+    // Turn off performance processing because we utilize
+    // our own hints via the FileSizeReporter
+    performance: false,
+  };
+};

+ 9 - 0
config/webpack/persistentCache/createEnvironmentHash.js

@@ -0,0 +1,9 @@
+'use strict';
+const { createHash } = require('crypto');
+
+module.exports = env => {
+  const hash = createHash('md5');
+  hash.update(JSON.stringify(env));
+
+  return hash.digest('hex');
+};

+ 127 - 0
config/webpackDevServer.config.js

@@ -0,0 +1,127 @@
+'use strict';
+
+const fs = require('fs');
+const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
+const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
+const ignoredFiles = require('react-dev-utils/ignoredFiles');
+const redirectServedPath = require('react-dev-utils/redirectServedPathMiddleware');
+const paths = require('./paths');
+const getHttpsConfig = require('./getHttpsConfig');
+
+const host = process.env.HOST || '0.0.0.0';
+const sockHost = process.env.WDS_SOCKET_HOST;
+const sockPath = process.env.WDS_SOCKET_PATH; // default: '/ws'
+const sockPort = process.env.WDS_SOCKET_PORT;
+
+module.exports = function (proxy, allowedHost) {
+  const disableFirewall =
+    !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true';
+  return {
+    // WebpackDevServer 2.4.3 introduced a security fix that prevents remote
+    // websites from potentially accessing local content through DNS rebinding:
+    // https://github.com/webpack/webpack-dev-server/issues/887
+    // https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
+    // However, it made several existing use cases such as development in cloud
+    // environment or subdomains in development significantly more complicated:
+    // https://github.com/facebook/create-react-app/issues/2271
+    // https://github.com/facebook/create-react-app/issues/2233
+    // While we're investigating better solutions, for now we will take a
+    // compromise. Since our WDS configuration only serves files in the `public`
+    // folder we won't consider accessing them a vulnerability. However, if you
+    // use the `proxy` feature, it gets more dangerous because it can expose
+    // remote code execution vulnerabilities in backends like Django and Rails.
+    // So we will disable the host check normally, but enable it if you have
+    // specified the `proxy` setting. Finally, we let you override it if you
+    // really know what you're doing with a special environment variable.
+    // Note: ["localhost", ".localhost"] will support subdomains - but we might
+    // want to allow setting the allowedHosts manually for more complex setups
+    allowedHosts: disableFirewall ? 'all' : [allowedHost],
+    headers: {
+      'Access-Control-Allow-Origin': '*',
+      'Access-Control-Allow-Methods': '*',
+      'Access-Control-Allow-Headers': '*',
+    },
+    // Enable gzip compression of generated files.
+    compress: true,
+    static: {
+      // By default WebpackDevServer serves physical files from current directory
+      // in addition to all the virtual build products that it serves from memory.
+      // This is confusing because those files won’t automatically be available in
+      // production build folder unless we copy them. However, copying the whole
+      // project directory is dangerous because we may expose sensitive files.
+      // Instead, we establish a convention that only files in `public` directory
+      // get served. Our build script will copy `public` into the `build` folder.
+      // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
+      // <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
+      // In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
+      // Note that we only recommend to use `public` folder as an escape hatch
+      // for files like `favicon.ico`, `manifest.json`, and libraries that are
+      // for some reason broken when imported through webpack. If you just want to
+      // use an image, put it in `src` and `import` it from JavaScript instead.
+      directory: paths.appPublic,
+      publicPath: [paths.publicUrlOrPath],
+      // By default files from `contentBase` will not trigger a page reload.
+      watch: {
+        // Reportedly, this avoids CPU overload on some systems.
+        // https://github.com/facebook/create-react-app/issues/293
+        // src/node_modules is not ignored to support absolute imports
+        // https://github.com/facebook/create-react-app/issues/1065
+        ignored: ignoredFiles(paths.appSrc),
+      },
+    },
+    client: {
+      webSocketURL: {
+        // Enable custom sockjs pathname for websocket connection to hot reloading server.
+        // Enable custom sockjs hostname, pathname and port for websocket connection
+        // to hot reloading server.
+        hostname: sockHost,
+        pathname: sockPath,
+        port: sockPort,
+      },
+      overlay: {
+        errors: true,
+        warnings: false,
+      },
+    },
+    devMiddleware: {
+      // It is important to tell WebpackDevServer to use the same "publicPath" path as
+      // we specified in the webpack config. When homepage is '.', default to serving
+      // from the root.
+      // remove last slash so user can land on `/test` instead of `/test/`
+      publicPath: paths.publicUrlOrPath.slice(0, -1),
+    },
+
+    https: getHttpsConfig(),
+    host,
+    historyApiFallback: {
+      // Paths with dots should still use the history fallback.
+      // See https://github.com/facebook/create-react-app/issues/387.
+      disableDotRule: true,
+      index: paths.publicUrlOrPath,
+    },
+    // `proxy` is run between `before` and `after` `webpack-dev-server` hooks
+    proxy,
+    onBeforeSetupMiddleware(devServer) {
+      // Keep `evalSourceMapMiddleware`
+      // middlewares before `redirectServedPath` otherwise will not have any effect
+      // This lets us fetch source contents from webpack for the error overlay
+      devServer.app.use(evalSourceMapMiddleware(devServer));
+
+      if (fs.existsSync(paths.proxySetup)) {
+        // This registers user provided middleware for proxy reasons
+        require(paths.proxySetup)(devServer.app);
+      }
+    },
+    onAfterSetupMiddleware(devServer) {
+      // Redirect to `PUBLIC_URL` or `homepage` from `package.json` if url not match
+      devServer.app.use(redirectServedPath(paths.publicUrlOrPath));
+
+      // This service worker file is effectively a 'no-op' that will reset any
+      // previous service worker registered for the same host:port combination.
+      // We do this in development to avoid hitting the production cache if
+      // it used the same host and port.
+      // https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
+      devServer.app.use(noopServiceWorkerMiddleware(paths.publicUrlOrPath));
+    },
+  };
+};

+ 160 - 0
package.json

@@ -0,0 +1,160 @@
+{
+  "name": "dyh-evaluation-admin",
+  "version": "1.2.6",
+  "private": true,
+  "dependencies": {
+    "@ant-design/icons": "^5.1.4",
+    "@babel/core": "^7.16.0",
+    "@dage/pc-components": "^1.3.2",
+    "@dage/service": "^1.0.3",
+    "@dage/utils": "^1.0.2",
+    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
+    "@svgr/webpack": "^5.5.0",
+    "@testing-library/jest-dom": "^5.16.5",
+    "@testing-library/react": "^13.4.0",
+    "@testing-library/user-event": "^13.5.0",
+    "@types/jest": "^27.5.2",
+    "@types/node": "^16.18.38",
+    "@types/react": "^18.2.16",
+    "@types/react-dom": "^18.2.6",
+    "antd": "^5.8.2",
+    "babel-jest": "^27.4.2",
+    "babel-loader": "^8.2.3",
+    "babel-plugin-named-asset-import": "^0.3.8",
+    "babel-preset-react-app": "^10.0.1",
+    "bfj": "^7.0.2",
+    "browserslist": "^4.18.1",
+    "camelcase": "^6.2.1",
+    "case-sensitive-paths-webpack-plugin": "^2.4.0",
+    "classnames": "^2.5.1",
+    "css-loader": "^6.5.1",
+    "css-minimizer-webpack-plugin": "^3.2.0",
+    "dotenv": "^10.0.0",
+    "dotenv-expand": "^5.1.0",
+    "eslint": "^8.3.0",
+    "eslint-config-react-app": "^7.0.1",
+    "eslint-webpack-plugin": "^3.1.1",
+    "file-loader": "^6.2.0",
+    "fs-extra": "^10.0.0",
+    "html-webpack-plugin": "^5.5.0",
+    "identity-obj-proxy": "^3.0.0",
+    "jest": "^27.4.3",
+    "jest-resolve": "^27.4.2",
+    "jest-watch-typeahead": "^1.0.0",
+    "js-base64": "^3.7.5",
+    "lodash": "^4.17.21",
+    "mini-css-extract-plugin": "^2.4.5",
+    "postcss": "^8.4.4",
+    "postcss-flexbugs-fixes": "^5.0.2",
+    "postcss-loader": "^6.2.1",
+    "postcss-normalize": "^10.0.1",
+    "postcss-preset-env": "^7.0.1",
+    "prompts": "^2.4.2",
+    "react": "^18.2.0",
+    "react-app-polyfill": "^3.0.0",
+    "react-dev-utils": "^12.0.1",
+    "react-dom": "^18.2.0",
+    "react-redux": "^8.1.1",
+    "react-refresh": "^0.11.0",
+    "react-router-dom": "^6.14.1",
+    "redux": "^4.2.1",
+    "redux-devtools-extension": "^2.13.9",
+    "redux-thunk": "^2.4.2",
+    "resolve": "^1.20.0",
+    "resolve-url-loader": "^4.0.0",
+    "sass-loader": "^12.3.0",
+    "semver": "^7.3.5",
+    "source-map-loader": "^3.0.0",
+    "style-loader": "^3.3.1",
+    "tailwindcss": "^3.0.2",
+    "terser-webpack-plugin": "^5.2.5",
+    "typescript": "^4.9.5",
+    "web-vitals": "^2.1.4",
+    "webpack": "^5.64.4",
+    "webpack-dev-server": "^4.6.0",
+    "webpack-manifest-plugin": "^4.0.2",
+    "workbox-webpack-plugin": "^6.4.1"
+  },
+  "scripts": {
+    "start": "cross-env REACT_APP_API_URL=https://sit-shgybwg.4dage.com node scripts/start.js",
+    "build": "cross-env PUBLIC_URL=./ REACT_APP_API_URL=https://sit-shgybwg.4dage.com node scripts/build.js"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  },
+  "devDependencies": {
+    "@types/lodash": "^4.14.195",
+    "babel-plugin-import": "^1.13.6",
+    "cross-env": "^7.0.3",
+    "sass": "^1.63.6"
+  },
+  "jest": {
+    "roots": [
+      "<rootDir>/src"
+    ],
+    "collectCoverageFrom": [
+      "src/**/*.{js,jsx,ts,tsx}",
+      "!src/**/*.d.ts"
+    ],
+    "setupFiles": [
+      "react-app-polyfill/jsdom"
+    ],
+    "setupFilesAfterEnv": [],
+    "testMatch": [
+      "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
+      "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
+    ],
+    "testEnvironment": "jsdom",
+    "transform": {
+      "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js",
+      "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
+      "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
+    },
+    "transformIgnorePatterns": [
+      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
+      "^.+\\.module\\.(css|sass|scss)$"
+    ],
+    "modulePaths": [],
+    "moduleNameMapper": {
+      "^react-native$": "react-native-web",
+      "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
+    },
+    "moduleFileExtensions": [
+      "web.js",
+      "js",
+      "web.ts",
+      "ts",
+      "web.tsx",
+      "tsx",
+      "json",
+      "web.jsx",
+      "jsx",
+      "node"
+    ],
+    "watchPlugins": [
+      "jest-watch-typeahead/filename",
+      "jest-watch-typeahead/testname"
+    ],
+    "resetMocks": true
+  },
+  "babel": {
+    "presets": [
+      "react-app"
+    ]
+  }
+}

File diff suppressed because it is too large
+ 12604 - 0
pnpm-lock.yaml


BIN
public/favicon.ico


BIN
public/fonts/SOURCEHANSERIFCN-BOLD.OTF


BIN
public/fonts/SOURCEHANSERIFCN-REGULAR.OTF


+ 23 - 0
public/index.html

@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>博物馆运行考核评估系统</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>

+ 217 - 0
scripts/build.js

@@ -0,0 +1,217 @@
+'use strict';
+
+// Do this as the first thing so that any code reading it knows the right env.
+process.env.BABEL_ENV = 'production';
+process.env.NODE_ENV = 'production';
+
+// Makes the script crash on unhandled rejections instead of silently
+// ignoring them. In the future, promise rejections that are not handled will
+// terminate the Node.js process with a non-zero exit code.
+process.on('unhandledRejection', err => {
+  throw err;
+});
+
+// Ensure environment variables are read.
+require('../config/env');
+
+const path = require('path');
+const chalk = require('react-dev-utils/chalk');
+const fs = require('fs-extra');
+const bfj = require('bfj');
+const webpack = require('webpack');
+const configFactory = require('../config/webpack.config');
+const paths = require('../config/paths');
+const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
+const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
+const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
+const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
+const printBuildError = require('react-dev-utils/printBuildError');
+
+const measureFileSizesBeforeBuild =
+  FileSizeReporter.measureFileSizesBeforeBuild;
+const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
+const useYarn = fs.existsSync(paths.yarnLockFile);
+
+// These sizes are pretty large. We'll warn for bundles exceeding them.
+const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
+const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
+
+const isInteractive = process.stdout.isTTY;
+
+// Warn and crash if required files are missing
+if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
+  process.exit(1);
+}
+
+const argv = process.argv.slice(2);
+const writeStatsJson = argv.indexOf('--stats') !== -1;
+
+// Generate configuration
+const config = configFactory('production');
+
+// We require that you explicitly set browsers and do not fall back to
+// browserslist defaults.
+const { checkBrowsers } = require('react-dev-utils/browsersHelper');
+checkBrowsers(paths.appPath, isInteractive)
+  .then(() => {
+    // First, read the current file sizes in build directory.
+    // This lets us display how much they changed later.
+    return measureFileSizesBeforeBuild(paths.appBuild);
+  })
+  .then(previousFileSizes => {
+    // Remove all content but keep the directory so that
+    // if you're in it, you don't end up in Trash
+    fs.emptyDirSync(paths.appBuild);
+    // Merge with the public folder
+    copyPublicFolder();
+    // Start the webpack build
+    return build(previousFileSizes);
+  })
+  .then(
+    ({ stats, previousFileSizes, warnings }) => {
+      if (warnings.length) {
+        console.log(chalk.yellow('Compiled with warnings.\n'));
+        console.log(warnings.join('\n\n'));
+        console.log(
+          '\nSearch for the ' +
+            chalk.underline(chalk.yellow('keywords')) +
+            ' to learn more about each warning.'
+        );
+        console.log(
+          'To ignore, add ' +
+            chalk.cyan('// eslint-disable-next-line') +
+            ' to the line before.\n'
+        );
+      } else {
+        console.log(chalk.green('Compiled successfully.\n'));
+      }
+
+      console.log('File sizes after gzip:\n');
+      printFileSizesAfterBuild(
+        stats,
+        previousFileSizes,
+        paths.appBuild,
+        WARN_AFTER_BUNDLE_GZIP_SIZE,
+        WARN_AFTER_CHUNK_GZIP_SIZE
+      );
+      console.log();
+
+      const appPackage = require(paths.appPackageJson);
+      const publicUrl = paths.publicUrlOrPath;
+      const publicPath = config.output.publicPath;
+      const buildFolder = path.relative(process.cwd(), paths.appBuild);
+      printHostingInstructions(
+        appPackage,
+        publicUrl,
+        publicPath,
+        buildFolder,
+        useYarn
+      );
+    },
+    err => {
+      const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
+      if (tscCompileOnError) {
+        console.log(
+          chalk.yellow(
+            'Compiled with the following type errors (you may want to check these before deploying your app):\n'
+          )
+        );
+        printBuildError(err);
+      } else {
+        console.log(chalk.red('Failed to compile.\n'));
+        printBuildError(err);
+        process.exit(1);
+      }
+    }
+  )
+  .catch(err => {
+    if (err && err.message) {
+      console.log(err.message);
+    }
+    process.exit(1);
+  });
+
+// Create the production build and print the deployment instructions.
+function build(previousFileSizes) {
+  console.log('Creating an optimized production build...');
+
+  const compiler = webpack(config);
+  return new Promise((resolve, reject) => {
+    compiler.run((err, stats) => {
+      let messages;
+      if (err) {
+        if (!err.message) {
+          return reject(err);
+        }
+
+        let errMessage = err.message;
+
+        // Add additional information for postcss errors
+        if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
+          errMessage +=
+            '\nCompileError: Begins at CSS selector ' +
+            err['postcssNode'].selector;
+        }
+
+        messages = formatWebpackMessages({
+          errors: [errMessage],
+          warnings: [],
+        });
+      } else {
+        messages = formatWebpackMessages(
+          stats.toJson({ all: false, warnings: true, errors: true })
+        );
+      }
+      if (messages.errors.length) {
+        // Only keep the first error. Others are often indicative
+        // of the same problem, but confuse the reader with noise.
+        if (messages.errors.length > 1) {
+          messages.errors.length = 1;
+        }
+        return reject(new Error(messages.errors.join('\n\n')));
+      }
+      if (
+        process.env.CI &&
+        (typeof process.env.CI !== 'string' ||
+          process.env.CI.toLowerCase() !== 'false') &&
+        messages.warnings.length
+      ) {
+        // Ignore sourcemap warnings in CI builds. See #8227 for more info.
+        const filteredWarnings = messages.warnings.filter(
+          w => !/Failed to parse source map/.test(w)
+        );
+        if (filteredWarnings.length) {
+          console.log(
+            chalk.yellow(
+              '\nTreating warnings as errors because process.env.CI = true.\n' +
+                'Most CI servers set it automatically.\n'
+            )
+          );
+          return reject(new Error(filteredWarnings.join('\n\n')));
+        }
+      }
+
+      const resolveArgs = {
+        stats,
+        previousFileSizes,
+        warnings: messages.warnings,
+      };
+
+      if (writeStatsJson) {
+        return bfj
+          .write(paths.appBuild + '/bundle-stats.json', stats.toJson())
+          .then(() => resolve(resolveArgs))
+          .catch(error => reject(new Error(error)));
+      }
+
+      return resolve(resolveArgs);
+    });
+  });
+}
+
+function copyPublicFolder() {
+  fs.copySync(paths.appPublic, paths.appBuild, {
+    dereference: true,
+    filter: file => file !== paths.appHtml,
+  });
+}

+ 154 - 0
scripts/start.js

@@ -0,0 +1,154 @@
+'use strict';
+
+// Do this as the first thing so that any code reading it knows the right env.
+process.env.BABEL_ENV = 'development';
+process.env.NODE_ENV = 'development';
+
+// Makes the script crash on unhandled rejections instead of silently
+// ignoring them. In the future, promise rejections that are not handled will
+// terminate the Node.js process with a non-zero exit code.
+process.on('unhandledRejection', err => {
+  throw err;
+});
+
+// Ensure environment variables are read.
+require('../config/env');
+
+const fs = require('fs');
+const chalk = require('react-dev-utils/chalk');
+const webpack = require('webpack');
+const WebpackDevServer = require('webpack-dev-server');
+const clearConsole = require('react-dev-utils/clearConsole');
+const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
+const {
+  choosePort,
+  createCompiler,
+  prepareProxy,
+  prepareUrls,
+} = require('react-dev-utils/WebpackDevServerUtils');
+const openBrowser = require('react-dev-utils/openBrowser');
+const semver = require('semver');
+const paths = require('../config/paths');
+const configFactory = require('../config/webpack.config');
+const createDevServerConfig = require('../config/webpackDevServer.config');
+const getClientEnvironment = require('../config/env');
+const react = require(require.resolve('react', { paths: [paths.appPath] }));
+
+const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
+const useYarn = fs.existsSync(paths.yarnLockFile);
+const isInteractive = process.stdout.isTTY;
+
+// Warn and crash if required files are missing
+if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
+  process.exit(1);
+}
+
+// Tools like Cloud9 rely on this.
+const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
+const HOST = process.env.HOST || '0.0.0.0';
+
+if (process.env.HOST) {
+  console.log(
+    chalk.cyan(
+      `Attempting to bind to HOST environment variable: ${chalk.yellow(
+        chalk.bold(process.env.HOST)
+      )}`
+    )
+  );
+  console.log(
+    `If this was unintentional, check that you haven't mistakenly set it in your shell.`
+  );
+  console.log(
+    `Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}`
+  );
+  console.log();
+}
+
+// We require that you explicitly set browsers and do not fall back to
+// browserslist defaults.
+const { checkBrowsers } = require('react-dev-utils/browsersHelper');
+checkBrowsers(paths.appPath, isInteractive)
+  .then(() => {
+    // We attempt to use the default port but if it is busy, we offer the user to
+    // run on a different port. `choosePort()` Promise resolves to the next free port.
+    return choosePort(HOST, DEFAULT_PORT);
+  })
+  .then(port => {
+    if (port == null) {
+      // We have not found a port.
+      return;
+    }
+
+    const config = configFactory('development');
+    const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
+    const appName = require(paths.appPackageJson).name;
+
+    const useTypeScript = fs.existsSync(paths.appTsConfig);
+    const urls = prepareUrls(
+      protocol,
+      HOST,
+      port,
+      paths.publicUrlOrPath.slice(0, -1)
+    );
+    // Create a webpack compiler that is configured with custom messages.
+    const compiler = createCompiler({
+      appName,
+      config,
+      urls,
+      useYarn,
+      useTypeScript,
+      webpack,
+    });
+    // Load proxy config
+    const proxySetting = require(paths.appPackageJson).proxy;
+    const proxyConfig = prepareProxy(
+      proxySetting,
+      paths.appPublic,
+      paths.publicUrlOrPath
+    );
+    // Serve webpack assets generated by the compiler over a web server.
+    const serverConfig = {
+      ...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
+      host: HOST,
+      port,
+    };
+    const devServer = new WebpackDevServer(serverConfig, compiler);
+    // Launch WebpackDevServer.
+    devServer.startCallback(() => {
+      if (isInteractive) {
+        clearConsole();
+      }
+
+      if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) {
+        console.log(
+          chalk.yellow(
+            `Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
+          )
+        );
+      }
+
+      console.log(chalk.cyan('Starting the development server...\n'));
+      openBrowser(urls.localUrlForBrowser);
+    });
+
+    ['SIGINT', 'SIGTERM'].forEach(function (sig) {
+      process.on(sig, function () {
+        devServer.close();
+        process.exit();
+      });
+    });
+
+    if (process.env.CI !== 'true') {
+      // Gracefully exit when stdin ends
+      process.stdin.on('end', function () {
+        devServer.close();
+        process.exit();
+      });
+    }
+  })
+  .catch(err => {
+    if (err && err.message) {
+      console.log(err.message);
+    }
+    process.exit(1);
+  });

+ 52 - 0
scripts/test.js

@@ -0,0 +1,52 @@
+'use strict';
+
+// Do this as the first thing so that any code reading it knows the right env.
+process.env.BABEL_ENV = 'test';
+process.env.NODE_ENV = 'test';
+process.env.PUBLIC_URL = '';
+
+// Makes the script crash on unhandled rejections instead of silently
+// ignoring them. In the future, promise rejections that are not handled will
+// terminate the Node.js process with a non-zero exit code.
+process.on('unhandledRejection', err => {
+  throw err;
+});
+
+// Ensure environment variables are read.
+require('../config/env');
+
+const jest = require('jest');
+const execSync = require('child_process').execSync;
+let argv = process.argv.slice(2);
+
+function isInGitRepository() {
+  try {
+    execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+function isInMercurialRepository() {
+  try {
+    execSync('hg --cwd . root', { stdio: 'ignore' });
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+// Watch unless on CI or explicitly running all tests
+if (
+  !process.env.CI &&
+  argv.indexOf('--watchAll') === -1 &&
+  argv.indexOf('--watchAll=false') === -1
+) {
+  // https://github.com/facebook/create-react-app/issues/5210
+  const hasSourceControl = isInGitRepository() || isInMercurialRepository();
+  argv.push(hasSourceControl ? '--watch' : '--watchAll');
+}
+
+
+jest.run(argv);

+ 93 - 0
src/App.scss

@@ -0,0 +1,93 @@
+@import "./theme.scss";
+
+:root {
+  --primary-color: #{$primaryColor};
+  --second-color: #{$secondColor};
+  --border-color: #ececec;
+  --index-normal: 1;
+  --index-top: 1000;
+  --index-popper: 2000;
+}
+
+body,
+ol,
+ul,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+th,
+td,
+dl,
+dd,
+form,
+fieldset,
+legend,
+input,
+textarea,
+select {
+  margin: 0;
+  padding: 0;
+}
+
+ul {
+  list-style: none;
+}
+
+body {
+  font-family: Source Han Sans CN-Regular;
+  font-size: 16px;
+}
+
+.limit-line {
+  display: -webkit-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+  word-break: break-all;
+  word-wrap: break-word;
+}
+
+.line-2 {
+  -webkit-line-clamp: 2;
+}
+
+.w220 {
+  width: 220px;
+}
+
+.w450 {
+  width: 450px;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+@font-face {
+  font-family: "Source Han Serif CN-Bold";
+  src: url("~/public/fonts/SOURCEHANSERIFCN-BOLD.OTF") format("opentype");
+  font-display: swap;
+}
+
+@font-face {
+  font-family: "Source Han Sans CN-Regular";
+  src: url("~/public/fonts/SOURCEHANSERIFCN-REGULAR.OTF") format("opentype");
+  font-display: swap;
+}
+
+.ant-btn {
+  box-shadow: none !important;
+}
+
+.ant-input:focus {
+  box-shadow: none;
+}
+
+.page-table {
+  margin-top: 20px;
+}

+ 40 - 0
src/App.tsx

@@ -0,0 +1,40 @@
+import React from "react";
+import { Route, Routes } from "react-router-dom";
+import { ConfigProvider } from "antd";
+import zhCN from "antd/lib/locale/zh_CN";
+import { DageLoading } from "@dage/pc-components";
+import theme from "./theme.scss";
+import "./App.scss";
+import "./configure";
+
+const Login = React.lazy(() => import("./pages/Login"));
+const Layout = React.lazy(() => import("./pages/Layout"));
+
+const Zhlocale: typeof zhCN = zhCN;
+if (Zhlocale.DatePicker?.lang) {
+  Zhlocale.DatePicker.lang = {
+    ...Zhlocale.DatePicker.lang,
+    monthFormat: "M月",
+    shortWeekDays: ["日", "一", "二", "三", "四", "五", "六"],
+  };
+}
+
+function App() {
+  return (
+    <div className="App">
+      <ConfigProvider
+        locale={Zhlocale}
+        theme={{ token: { colorPrimary: theme.primaryColor } }}
+      >
+        <React.Suspense fallback={<DageLoading />}>
+          <Routes>
+            <Route path="/login" Component={Login} />
+            <Route path="/*" Component={Layout} />
+          </Routes>
+        </React.Suspense>
+      </ConfigProvider>
+    </div>
+  );
+}
+
+export default App;

+ 17 - 0
src/api/index.ts

@@ -0,0 +1,17 @@
+import { LoginRequest, LoginResponse, UpdatePwdRequest } from "@/types";
+import { requestByGet, requestByPost } from "@dage/service";
+
+export const login = (data: LoginRequest) => {
+  return requestByPost<LoginResponse>("/api/admin/login", data);
+};
+
+export const logoutApi = () => {
+  return requestByGet("/api/admin/logout");
+};
+
+export const updatePwd = (data: UpdatePwdRequest) => {
+  return requestByPost("/api/sys/user/updatePwd", data);
+};
+
+export * from "./log";
+export * from "./user";

+ 8 - 0
src/api/log.ts

@@ -0,0 +1,8 @@
+import { GetLogListParams } from "@/types";
+import { request } from "@dage/service";
+
+export const logApi = {
+  getList(params: GetLogListParams) {
+    return request("/api/sys/log/list", params);
+  },
+};

+ 23 - 0
src/api/user.ts

@@ -0,0 +1,23 @@
+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);
+  },
+};

BIN
src/assets/images/logo.png


+ 22 - 0
src/components/FormPageFooter/index.scss

@@ -0,0 +1,22 @@
+.form-page-footer {
+  $height: 60px;
+  height: calc($height);
+
+  &-container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: fixed;
+    left: 220px;
+    right: 0;
+    bottom: 0;
+    height: $height;
+    background: white;
+    border-top: 1px solid var(--border-color);
+    z-index: 1;
+
+    > *:not(:first-child) {
+      margin-left: 20px;
+    }
+  }
+}

+ 52 - 0
src/components/FormPageFooter/index.tsx

@@ -0,0 +1,52 @@
+import { Button, message } from "antd";
+import "./index.scss";
+import { FC, memo, useCallback, useState } from "react";
+
+export interface FormPageFooterProps {
+  disabled?: boolean;
+  showSubmit?: boolean;
+  onSubmit?(): void;
+  onCancel?(): void;
+}
+
+export const FormPageFooter: FC<FormPageFooterProps> = memo(
+  ({ onSubmit, onCancel, disabled, showSubmit = true }) => {
+    const [loading, setLoading] = useState(false);
+
+    const handleClick = useCallback(async () => {
+      setLoading(true);
+      try {
+        await onSubmit?.();
+
+        message.open({
+          type: "success",
+          content: "操作成功",
+        });
+      } finally {
+        setLoading(false);
+      }
+    }, [onSubmit]);
+
+    const handleCancel = useCallback(() => {
+      onCancel?.();
+    }, [onCancel]);
+
+    return (
+      <div className="form-page-footer">
+        <div className="form-page-footer-container">
+          {showSubmit && (
+            <Button
+              disabled={disabled}
+              loading={loading}
+              type="primary"
+              onClick={handleClick}
+            >
+              提交
+            </Button>
+          )}
+          <Button onClick={handleCancel}>取消</Button>
+        </div>
+      </div>
+    );
+  }
+);

+ 26 - 0
src/components/NotFound/index.tsx

@@ -0,0 +1,26 @@
+import { Result } from "antd";
+import { useEffect, useRef } from "react";
+
+export default function NotFound() {
+  const timeRef = useRef(-1);
+
+  useEffect(() => {
+    timeRef.current = window.setTimeout(() => {
+      const dom: HTMLDivElement = document.querySelector(".noFindPage")!;
+      dom.style.opacity = "1";
+    }, 300);
+    return () => {
+      clearTimeout(timeRef.current);
+    };
+  }, []);
+
+  return (
+    <div className="noFindPage">
+      <Result
+        status="404"
+        title="404"
+        subTitle="页面找不到或没有权限,请联系管理员!"
+      />
+    </div>
+  );
+}

+ 1 - 0
src/components/index.ts

@@ -0,0 +1 @@
+export * from "./FormPageFooter";

+ 64 - 0
src/configure.ts

@@ -0,0 +1,64 @@
+import { compose, initial } from "@dage/service";
+import { getTokenInfo, removeTokenInfo } from "@dage/pc-components";
+import { message } from "antd";
+import { NoticeType } from "antd/es/message/interface";
+import { ResponseStatusCode } from "./types";
+
+/**
+ * 定义请求元数据
+ */
+declare global {
+  interface DageRequestMeta {
+    /**
+     * 显示全局 错误信息, 默认为 false
+     */
+    showError?: boolean;
+  }
+}
+
+const showMessage = (msg: string, type: NoticeType = "error") => {
+  message.open({
+    type,
+    content: msg,
+    duration: 4,
+  });
+};
+
+initial({
+  fetch: window.fetch.bind(window),
+  baseURL: process.env.REACT_APP_API_URL as string,
+  interceptor: compose(
+    (request, next) => {
+      const { token } = getTokenInfo();
+      if (token) {
+        request.headers["token"] = token;
+      }
+      return next();
+    },
+    // 登陆失效
+    async (request, next) => {
+      const response = await next();
+      const { showError = true } = request.meta;
+
+      if (
+        [
+          ResponseStatusCode.TOKEN_INVALID,
+          ResponseStatusCode.TOKEN_INVALID2,
+        ].includes(response.code)
+      ) {
+        showMessage("登录失效!");
+        removeTokenInfo();
+        globalThis.location.href = "#/login";
+      } else if (response.code !== ResponseStatusCode.SUCCESS) {
+        const message = response.__raw__.data.msg ?? "系统出差中";
+        // 错误信息映射
+        response.errorMessage = message;
+        if (showError) {
+          showMessage(message);
+        }
+      }
+
+      return response;
+    }
+  ),
+});

+ 9 - 0
src/css.d.ts

@@ -0,0 +1,9 @@
+declare module "*.module.scss" {
+  const classes: { readonly [key: string]: string };
+  export default classes;
+}
+
+declare module "*.scss" {
+  const classes: { readonly [key: string]: string };
+  export default classes;
+}

+ 7 - 0
src/img.d.ts

@@ -0,0 +1,7 @@
+declare module "*.svg";
+declare module "*.png";
+declare module "*.jpg";
+declare module "*.jpeg";
+declare module "*.gif";
+declare module "*.bmp";
+declare module "*.tiff";

+ 19 - 0
src/index.tsx

@@ -0,0 +1,19 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { HashRouter } from "react-router-dom";
+import { Provider } from "react-redux";
+import store from "./store/index";
+import App from "./App";
+
+const root = ReactDOM.createRoot(
+  document.getElementById("root") as HTMLElement
+);
+root.render(
+  <React.StrictMode>
+    <HashRouter>
+      <Provider store={store}>
+        <App />
+      </Provider>
+    </HashRouter>
+  </React.StrictMode>
+);

+ 11 - 0
src/pages/Assessment/Index/index.module.scss

@@ -0,0 +1,11 @@
+.page {
+  display: flex;
+  min-height: 100%;
+}
+
+.sidebar {
+  width: 338px;
+  min-height: 100%;
+  border-radius: 2px;
+  background: rgba(165, 181, 210, 0.1);
+}

+ 59 - 0
src/pages/Assessment/Index/index.tsx

@@ -0,0 +1,59 @@
+import { TreeDataNode } from "antd";
+import { DataNode } from "antd/es/tree";
+import { DageTreeActions } from "@dage/pc-components";
+import style from "./index.module.scss";
+
+const treeData: TreeDataNode[] = [
+  {
+    title: "parent 1",
+    key: "0-0",
+    children: [
+      {
+        title: "leaf",
+        key: "0-0-0",
+      },
+      {
+        title: "leaf",
+        key: "0-0-1",
+        children: [
+          {
+            title: "leaf leaf",
+            key: "0-1-1",
+          },
+        ],
+      },
+    ],
+  },
+];
+
+let key = 0;
+
+const IndexPage = () => {
+  const handleAddNode = async (item: DataNode) => {
+    return new Promise((res) => {
+      setTimeout(() => {
+        if (!item.children) {
+          item.children = [
+            {
+              title: "leaf",
+              key: "0-0-0" + key++,
+            },
+          ];
+        }
+        res(true);
+      }, 1000);
+    });
+  };
+
+  return (
+    <div className={style.page}>
+      <DageTreeActions
+        className={style.sidebar}
+        treeData={treeData}
+        onAdd={handleAddNode}
+      />
+    </div>
+  );
+};
+
+export default IndexPage;

+ 98 - 0
src/pages/Layout/components/Header/components/ResetPassword.tsx

@@ -0,0 +1,98 @@
+import { updatePwd } from "@/api";
+import { logout } from "@/utils";
+import { Base64 } from "@dage/utils";
+import { encodeStr } from "@dage/pc-components";
+import { Form, FormInstance, Input, Modal, ModalProps, message } from "antd";
+import { FC, useRef, useState } from "react";
+
+export interface ResetPasswordProps extends Pick<ModalProps, "onCancel"> {
+  visible: boolean;
+}
+
+export const ResetPassword: FC<ResetPasswordProps> = ({ visible, ...rest }) => {
+  // 拿到新密码的输入框的值
+  const oldPasswordValue = useRef("");
+  const formRef = useRef<FormInstance>(null);
+  const [confirmLoading, setConfirmLoading] = useState(false);
+
+  const checkPassWord = (rule: any, value = "") => {
+    if (value !== oldPasswordValue.current)
+      return Promise.reject("新密码不一致!");
+    else return Promise.resolve(value);
+  };
+
+  const onFinish = async () => {
+    setConfirmLoading(true);
+    try {
+      if (!(await formRef.current?.validateFields())) return;
+
+      const value = formRef.current?.getFieldsValue();
+      const obj = {
+        oldPassword: encodeStr(Base64.encode(value.oldPassword)),
+        newPassword: encodeStr(Base64.encode(value.newPassword)),
+      };
+      await updatePwd(obj);
+
+      message.open({
+        type: "success",
+        content: "修改成功!",
+      });
+      logout();
+    } finally {
+      setConfirmLoading(false);
+    }
+  };
+
+  return (
+    <Modal
+      open={visible}
+      title="修改密码"
+      {...rest}
+      maskClosable={false}
+      destroyOnClose={true}
+      confirmLoading={confirmLoading}
+      onOk={onFinish}
+    >
+      <Form
+        ref={formRef}
+        labelCol={{ span: 5 }}
+        wrapperCol={{ span: 16 }}
+        onFinish={onFinish}
+        autoComplete="off"
+      >
+        <Form.Item
+          label="旧密码"
+          name="oldPassword"
+          rules={[{ required: true, message: "不能为空!" }]}
+        >
+          <Input.Password maxLength={15} />
+        </Form.Item>
+
+        <Form.Item
+          label="新密码"
+          name="newPassword"
+          rules={[
+            { required: true, message: "不能为空!" },
+            { min: 6, max: 15, message: "密码长度为6-15个字符!" },
+          ]}
+        >
+          <Input.Password
+            maxLength={15}
+            onChange={(e) => (oldPasswordValue.current = e.target.value)}
+          />
+        </Form.Item>
+
+        <Form.Item
+          label="确定新密码"
+          name="checkPass"
+          rules={[
+            { required: true, message: "不能为空!" },
+            { validator: checkPassWord },
+          ]}
+        >
+          <Input.Password maxLength={15} />
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+};

+ 37 - 0
src/pages/Layout/components/Header/index.module.scss

@@ -0,0 +1,37 @@
+.header {
+  position: sticky;
+  top: 0;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 40px;
+  height: 60px;
+  line-height: 60px;
+  background-color: #fff;
+  border-bottom: 1px solid var(--border-color);
+  z-index: var(--index-top);
+}
+
+.user {
+  display: flex;
+  align-items: center;
+  font-size: 16px;
+  cursor: pointer;
+
+  span {
+    margin-left: 10px;
+  }
+}
+
+.userPopover {
+  li {
+    height: 50px;
+    line-height: 50px;
+    text-align: center;
+    cursor: pointer;
+
+    &:hover {
+      color: var(--second-color);
+    }
+  }
+}

+ 113 - 0
src/pages/Layout/components/Header/index.tsx

@@ -0,0 +1,113 @@
+import { FC, useCallback, useMemo, useState } from "react";
+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 { ResetPassword } from "./components/ResetPassword";
+import { logout } from "@/utils";
+import { DageRouteItem } from "@/router";
+import { useLocation } from "react-router-dom";
+import { findRouteByPath } from "../../utils";
+import { UserOutlined } from "@ant-design/icons";
+
+export interface LayoutHeaderProps {
+  /** 菜单内容 */
+  menuData: DageRouteItem[];
+}
+
+const isHashRoute = window.location.href.includes("#");
+const generateBreadcrumb = (
+  pathname: string,
+  child: DageRouteItem[]
+): {
+  title: string;
+  href?: string;
+}[] => {
+  if (!pathname || !child.length) return [];
+
+  const breadcrumb = [];
+  const paths = pathname.split("/").filter((path) => path !== "");
+  let currentPath = "";
+
+  for (let i = 0; i < paths.length; i++) {
+    currentPath += `/${paths[i]}`;
+    const item = findRouteByPath(child, currentPath);
+
+    if (item) {
+      breadcrumb.push({
+        title: item.title,
+        href:
+          i < paths.length - 1 && item.children
+            ? `${isHashRoute ? "#" : ""}${item.redirect || item.path}`
+            : undefined,
+      });
+
+      if (!item.children) break;
+    }
+  }
+
+  return breadcrumb;
+};
+
+export const LayoutHeader: FC<LayoutHeaderProps> = ({ menuData }) => {
+  const { modal } = App.useApp();
+  const location = useLocation();
+  const breadcrumbItems = useMemo(
+    () => generateBreadcrumb(location.pathname, menuData),
+    [location, menuData]
+  );
+  const [resetPwdModalVisible, setResetPwdModalVisible] = useState(false);
+  const { userInfo } = useSelector<RootState, RootState["base"]>(
+    (state) => state.base
+  );
+
+  const handleLogout = useCallback(() => {
+    modal.confirm({
+      title: "提示",
+      content: "确定退出吗?",
+      async onOk() {
+        await logout();
+      },
+    });
+  }, [modal]);
+
+  return (
+    <Header className={style.header}>
+      <Breadcrumb items={breadcrumbItems} />
+
+      <Popover
+        placement="bottom"
+        content={
+          <ul className={style.userPopover}>
+            <li>
+              <Button type="text" onClick={() => setResetPwdModalVisible(true)}>
+                修改密码
+              </Button>
+            </li>
+            <li>
+              <Button type="text" onClick={handleLogout}>
+                退出登录
+              </Button>
+            </li>
+          </ul>
+        }
+      >
+        <div className={style.user}>
+          <Avatar
+            size={40}
+            shape="square"
+            icon={<UserOutlined />}
+            alt={userInfo?.user.nickName}
+          />
+          <span>{userInfo?.user.nickName}</span>
+        </div>
+      </Popover>
+
+      <ResetPassword
+        visible={resetPwdModalVisible}
+        onCancel={() => setResetPwdModalVisible(false)}
+      />
+    </Header>
+  );
+};

+ 88 - 0
src/pages/Layout/components/Menu/index.tsx

@@ -0,0 +1,88 @@
+import { Menu, MenuProps } from "antd";
+import { FC, memo, useCallback, useMemo } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+// import { CaretDownOutlined } from "@ant-design/icons";
+import { DageRouteItem } from "@/router";
+import { findRouteByPath } from "../../utils";
+
+export interface LayoutMenuProps extends Omit<MenuProps, "mode" | "items"> {
+  /** 菜单内容 */
+  menuData: DageRouteItem[];
+  /**
+   * 最大可展示层级
+   * @default 2
+   */
+  maxShowLevel?: number;
+}
+
+export const LayoutMenu: FC<LayoutMenuProps> = memo(
+  ({ menuData, maxShowLevel = 2, ...rest }) => {
+    const location = useLocation();
+    const navigate = useNavigate();
+    const defaultOpenKeys = useMemo(() => {
+      let currentPath = "";
+      const stack: string[] = [];
+      const paths = location.pathname.split("/").filter((path) => path !== "");
+      for (let i = 0; i < paths.length; i++) {
+        currentPath += `/${paths[i]}`;
+        const item = findRouteByPath(menuData, currentPath);
+        if (item) {
+          stack.push(item.path);
+        }
+      }
+      return stack;
+    }, [menuData, location]);
+
+    const renderMenuItems = useCallback(
+      (list: DageRouteItem[], level = 1): any[] => {
+        const stack: DageRouteItem[] = [];
+
+        list.forEach((item) => {
+          let child: DageRouteItem[] = [];
+          const { title, path, icon, children, hide } = item;
+          const params = {
+            key: path,
+            icon: icon,
+            label: title,
+          };
+
+          if (hide) return null;
+
+          if (level <= maxShowLevel - 1 && children) {
+            child = renderMenuItems(children, level + 1);
+          }
+
+          if (child.length) {
+            // @ts-ignore
+            params.children = child;
+          }
+
+          // @ts-ignore
+          stack.push(params);
+        });
+
+        return stack;
+      },
+      [maxShowLevel]
+    );
+
+    const handleMenu = useCallback(
+      (item: any) => {
+        navigate(item.key);
+      },
+      [navigate]
+    );
+
+    return menuData.length ? (
+      <Menu
+        mode="inline"
+        // expandIcon={<CaretDownOutlined />}
+        items={renderMenuItems(menuData)}
+        selectedKeys={location.pathname.split("/").map((i) => `/${i}`)}
+        defaultOpenKeys={defaultOpenKeys}
+        onClick={handleMenu}
+        {...rest}
+      />
+    ) : null;
+  }
+);

+ 2 - 0
src/pages/Layout/components/index.ts

@@ -0,0 +1,2 @@
+export * from "./Header";
+export * from "./Menu";

+ 78 - 0
src/pages/Layout/index.scss

@@ -0,0 +1,78 @@
+.layout {
+  min-height: 100vh;
+
+  .logo {
+    padding: 20px 20px 0;
+
+    img {
+      width: 100%;
+    }
+  }
+}
+
+.layout-menu {
+  color: #ffffff;
+  font-size: 16px;
+  background: #242424;
+
+  .ant-menu-item,
+  .ant-menu-submenu {
+    margin: 0;
+    margin-block: 10px;
+    min-height: 40px;
+    line-height: 40px;
+
+    &-icon {
+      font-size: 20px !important;
+    }
+  }
+  .ant-menu-item {
+    &-selected {
+      color: #ffffff !important;
+      background: rgba(57, 57, 57, 0.5) !important;
+
+      .ant-menu-title-content {
+        color: inherit !important;
+      }
+      &::before {
+        content: "";
+        position: absolute;
+        top: 0;
+        left: 0;
+        bottom: 0;
+        width: 3px;
+        background: var(--primary-color);
+      }
+    }
+    &-active .ant-menu-title-content {
+      color: var(--second-color);
+    }
+  }
+  .ant-menu-submenu {
+    .ant-menu-sub {
+      background: inherit !important;
+
+      .ant-menu-title-content {
+        padding-left: 15px;
+      }
+      .ant-menu-item {
+        margin-inline: 0;
+        margin-block: 0;
+        width: 100%;
+        font-size: 12px;
+      }
+    }
+    &-selected {
+      .ant-menu-item-selected {
+        border-radius: 0;
+        color: #ffffff !important;
+        background: rgba(57, 57, 57, 0.5) !important;
+      }
+    }
+    &-title {
+      color: inherit !important;
+      height: inherit !important;
+      line-height: inherit !important;
+    }
+  }
+}

+ 129 - 0
src/pages/Layout/index.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() });
+    }
+  }, [navigate]);
+
+  return (
+    <App>
+      <Layout hasSider className="layout">
+        {/* 菜单 */}
+        <Layout.Sider
+          width={220}
+          style={{
+            position: "fixed",
+            top: 0,
+            left: 0,
+            bottom: 0,
+            background: "#242424",
+          }}
+        >
+          <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);
+}

+ 44 - 0
src/pages/Layout/utils.ts

@@ -0,0 +1,44 @@
+import { DageRouteItem } from "@/router";
+
+export const getParentMenuItem = (
+  menuItems: DageRouteItem[],
+  path: string
+): null | DageRouteItem => {
+  for (const menuItem of menuItems) {
+    if (path.startsWith(menuItem.path || "")) {
+      return menuItem;
+    }
+    if (menuItem.children) {
+      const parentMenuItem = getParentMenuItem(menuItem.children, path);
+      if (parentMenuItem) {
+        return parentMenuItem;
+      }
+    }
+  }
+  return null;
+};
+
+export const findRouteByPath = (
+  routes: DageRouteItem[],
+  path: string
+): DageRouteItem | null => {
+  const temp = path.split("/");
+
+  for (const route of routes) {
+    if (route.path.replace(/\/:[^/]+/g, "") === path) {
+      return route;
+    }
+    if (
+      // 首层没必要向下遍历
+      temp.length !== 2 &&
+      route.path.indexOf(temp[1]) > -1 &&
+      route.children
+    ) {
+      const subRoute = findRouteByPath(route.children, path);
+      if (subRoute) {
+        return subRoute;
+      }
+    }
+  }
+  return null;
+};

+ 141 - 0
src/pages/Log/index.tsx

@@ -0,0 +1,141 @@
+import { logApi } from "@/api";
+import { GetLogListParams } from "@/types";
+import { Button, DatePicker, Form, FormInstance, Input, Table } from "antd";
+import { debounce } from "lodash";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { formatDate } from "@dage/utils";
+
+const DEFAULT_PARAMS: GetLogListParams = {
+  pageNum: 1,
+  pageSize: 20,
+  startTime: "",
+  endTime: "",
+};
+
+const { RangePicker } = DatePicker;
+
+export default function IndustrialMeta() {
+  const formRef = useRef<FormInstance>(null);
+  const [list, setList] = useState([]);
+  const [total, setTotal] = useState(0);
+  const [loading, setLoading] = useState(false);
+  const [params, setParams] = useState<GetLogListParams>({
+    ...DEFAULT_PARAMS,
+  });
+
+  const getList = useCallback(async () => {
+    setLoading(true);
+    try {
+      const data = await logApi.getList(params);
+      setList(data.records);
+      setTotal(data.total);
+    } finally {
+      setLoading(false);
+    }
+  }, [params]);
+
+  useEffect(() => {
+    getList();
+  }, [getList, params]);
+
+  const debounceSearch = useMemo(
+    () =>
+      debounce(
+        (changedVal: unknown, vals: GetLogListParams & { date: string[] }) => {
+          const { date, ...rest } = vals;
+
+          let startTime = "";
+          let endTime = "";
+          if (date && date[0] && date[1]) {
+            startTime = formatDate(date[0]) + " 00:00:00";
+            endTime = formatDate(date[1]) + " 23:59:59";
+          }
+
+          setParams({ ...params, ...rest, startTime, endTime });
+        },
+        500
+      ),
+    [params]
+  );
+
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      setParams({ ...params, pageNum, pageSize });
+    },
+    [params]
+  );
+
+  const handleReset = useCallback(() => {
+    setParams({ ...DEFAULT_PARAMS });
+    formRef.current?.resetFields();
+  }, [formRef]);
+
+  const COLUMNS = useMemo(() => {
+    return [
+      {
+        title: "序号",
+        render: (text: any, record: any, index: any) =>
+          index + 1 + (params.pageNum - 1) * params.pageSize,
+      },
+      {
+        title: "操作者",
+        dataIndex: "userName",
+      },
+      {
+        title: "操作日期",
+        dataIndex: "createTime",
+      },
+      {
+        title: "IP记录",
+        dataIndex: "ip",
+      },
+      {
+        title: "操作模块",
+        dataIndex: "type",
+      },
+      {
+        title: "操作事件",
+        dataIndex: "description",
+      },
+    ];
+  }, [params]);
+
+  return (
+    <div className="log">
+      <Form ref={formRef} layout="inline" onValuesChange={debounceSearch}>
+        <Form.Item label="账号" name="searchKey">
+          <Input
+            className="w220"
+            placeholder="请输入关键字"
+            maxLength={30}
+            showCount
+            allowClear
+          />
+        </Form.Item>
+        <Form.Item label="操作日期" name="date">
+          <RangePicker />
+        </Form.Item>
+        <Form.Item>
+          <Button onClick={handleReset}>重置</Button>
+        </Form.Item>
+      </Form>
+
+      <Table
+        loading={loading}
+        className="page-table"
+        dataSource={list}
+        columns={COLUMNS}
+        rowKey="id"
+        pagination={{
+          showQuickJumper: true,
+          position: ["bottomCenter"],
+          showSizeChanger: true,
+          current: params.pageNum,
+          pageSize: params.pageSize,
+          total,
+          onChange: paginationChange(),
+        }}
+      />
+    </div>
+  );
+}

BIN
src/pages/Login/images/bg.jpg


BIN
src/pages/Login/images/icon_account.png


BIN
src/pages/Login/images/icon_password.png


+ 115 - 0
src/pages/Login/index.scss

@@ -0,0 +1,115 @@
+.login {
+  width: 100vw;
+  height: 100vh;
+  overflow: hidden;
+  background-image: url("./images/bg.jpg");
+  background-size: cover;
+
+  &::before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.4);
+    z-index: 1;
+  }
+  .icon-account,
+  .icon-password {
+    margin-right: 24px;
+    width: 26px;
+  }
+
+  &-logo {
+    display: flex;
+    flex-direction: column;
+    position: absolute;
+    top: 17.5vh;
+    left: 15vw;
+    z-index: 1;
+
+    &__txt {
+      display: flex;
+      justify-content: space-between;
+      margin-top: 23px;
+      font-size: 24px;
+      color: #ffffff;
+      opacity: 0.6;
+    }
+  }
+
+  &-form {
+    position: absolute;
+    top: 52%;
+    right: 12vw;
+    width: 550px;
+    height: 646px;
+    padding: 66px 75px;
+    background: rgba(176, 161, 121, 0.2);
+    box-shadow: inset 0px 4px 4px 0px rgba(255, 255, 255, 0.25);
+    border-radius: 3px;
+    transform: translateY(-50%);
+    z-index: 2;
+
+    &__title {
+      margin-bottom: 20px;
+      font-size: 40px;
+      font-family: Source Han Serif CN-Bold;
+      font-weight: bold;
+      color: white;
+    }
+
+    &__input {
+      $inputHeight: 85px;
+      display: flex;
+      flex-direction: column;
+
+      .ant-input {
+        width: 100%;
+        height: $inputHeight;
+        color: #ffffff;
+        background-color: transparent;
+
+        &::-webkit-input-placeholder {
+          color: #ffffff;
+          opacity: 0.6;
+        }
+        &:-webkit-autofill {
+          transition-delay: 99999s;
+        }
+        &:-webkit-autofill::first-line {
+          font-size: 24px;
+        }
+      }
+      .ant-input-affix-wrapper {
+        padding: 0 11px;
+        width: 100%;
+        height: $inputHeight;
+        border: none;
+        color: #ffffff;
+        font-size: 24px;
+        border-bottom: 1px solid #ffffff;
+        border-radius: 0;
+        background-color: transparent;
+      }
+      .ant-input-password-icon {
+        color: #ffffff !important;
+        opacity: 0.6;
+      }
+    }
+
+    &__btn {
+      margin-top: 85px;
+
+      .ant-btn {
+        font-size: 24px;
+        width: 100%;
+        height: 66px;
+        border-radius: 3px;
+        border: 0;
+        background: var(--second-color);
+      }
+    }
+  }
+}

+ 94 - 0
src/pages/Login/index.tsx

@@ -0,0 +1,94 @@
+import { useState } from "react";
+import { Button, Form, Input } from "antd";
+import { useNavigate } from "react-router-dom";
+import { LoginRequest } from "@/types";
+import { login } from "@/api";
+import { Base64 } from "@dage/utils";
+import { encodeStr, setTokenInfo } from "@dage/pc-components";
+import IconAccount from "./images/icon_account.png";
+import IconPassword from "./images/icon_password.png";
+import LogoImage from "../../assets/images/logo.png";
+import { DEFAULT_ADMIN_MENU, DEFAULT_MENU } from "@/router";
+import "./index.scss";
+
+export default function Login() {
+  const [loading, setLoading] = useState(false);
+  const navigate = useNavigate();
+
+  const handleLogin = async (vals: LoginRequest) => {
+    setLoading(true);
+
+    const obj = {
+      userName: vals.userName,
+      passWord: encodeStr(Base64.encode(vals.passWord as string)),
+    };
+
+    try {
+      const data = await login(obj);
+      const list = data.user.isAdmin
+        ? [...DEFAULT_MENU, ...DEFAULT_ADMIN_MENU]
+        : [...DEFAULT_MENU];
+
+      // 用户信息存到本地
+      setTokenInfo(data);
+      navigate(list[0].redirect || list[0].path, {
+        replace: true,
+      });
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="login">
+      <div className="login-logo">
+        <img alt="logo" src={LogoImage} />
+        <div className="login-logo__txt">
+          {"上海工业博物馆后台界面".split("").map((t) => (
+            <span key={t}>{t}</span>
+          ))}
+        </div>
+      </div>
+
+      <div className="login-form">
+        <div className="login-form__title">登 录</div>
+
+        <Form className="login-form__input" onFinish={handleLogin}>
+          <Form.Item
+            name="userName"
+            rules={[{ required: true, message: "请输入用户名!" }]}
+          >
+            <Input
+              prefix={
+                <img className="icon-account" src={IconAccount} alt="账号" />
+              }
+              placeholder="请输入用户名"
+              maxLength={15}
+              bordered={false}
+            />
+          </Form.Item>
+          <Form.Item
+            name="passWord"
+            rules={[{ required: true, message: "请输入密码!" }]}
+          >
+            <Input.Password
+              prefix={
+                <img className="icon-password" src={IconPassword} alt="密码" />
+              }
+              placeholder="请输入用户密码"
+              maxLength={15}
+              bordered={false}
+            />
+          </Form.Item>
+
+          {/* 登录按钮 */}
+          <div className="login-form__btn">
+            <Button htmlType="submit" loading={loading}>
+              登 录
+            </Button>
+          </div>
+        </Form>
+      </div>
+    </div>
+  );
+}

+ 19 - 0
src/pages/User/components/UserAdd/index.module.scss

@@ -0,0 +1,19 @@
+.userAdd {
+  :global {
+    .ant-modal-close {
+      display: none;
+    }
+
+    .userAddMain {
+      border-top: 1px solid #999999;
+      padding-top: 15px;
+      width: 100%;
+
+      .passTit {
+        color: #ff4d4f;
+        font-size: 14px;
+        padding-left: 98px;
+      }
+    }
+  }
+}

+ 141 - 0
src/pages/User/components/UserAdd/index.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;

+ 227 - 0
src/pages/User/index.tsx

@@ -0,0 +1,227 @@
+import { userApi } from "@/api";
+import { GetLogListParams, UserTableListType } from "@/types";
+import {
+  Button,
+  Form,
+  FormInstance,
+  Input,
+  Popconfirm,
+  Switch,
+  Table,
+  message,
+} from "antd";
+import { debounce } from "lodash";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import UserAdd from "./components/UserAdd";
+import { DageTableActions } from "@dage/pc-components";
+
+const DEFAULT_PARAMS: GetLogListParams = {
+  pageNum: 1,
+  pageSize: 20,
+  startTime: "",
+  endTime: "",
+};
+
+export default function IndustrialMeta() {
+  const formRef = useRef<FormInstance>(null);
+  const [list, setList] = useState<UserTableListType[]>([]);
+  const [total, setTotal] = useState(0);
+  const [loading, setLoading] = useState(false);
+  const [editPageShow, setEditPageShow] = useState(false);
+  const editId = useRef(0);
+  const [params, setParams] = useState<GetLogListParams>({
+    ...DEFAULT_PARAMS,
+  });
+
+  const getList = useCallback(async () => {
+    setLoading(true);
+    try {
+      const data = await userApi.getList(params);
+      setList(data.records);
+      setTotal(data.total);
+    } finally {
+      setLoading(false);
+    }
+  }, [params]);
+
+  useEffect(() => {
+    getList();
+  }, [getList, params]);
+
+  const debounceSearch = useMemo(
+    () =>
+      debounce((changedVal: unknown, vals: GetLogListParams) => {
+        setParams({ ...params, ...vals });
+      }, 500),
+    [params]
+  );
+
+  // 切换表格中的启用停用状态
+  const isEnabledClickFu = useCallback(
+    async (val: boolean, id: number) => {
+      const isDisable = val ? 1 : 0;
+      await userApi.handleType(id, isDisable);
+      getList();
+    },
+    [getList]
+  );
+
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      setParams({ ...params, pageNum, pageSize });
+    },
+    [params]
+  );
+
+  const handleReset = useCallback(() => {
+    setParams({ ...DEFAULT_PARAMS });
+    formRef.current?.resetFields();
+  }, [formRef]);
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: number) => {
+      await userApi.del(id);
+      message.open({
+        type: "success",
+        content: "操作成功",
+      });
+      getList();
+    },
+    [getList]
+  );
+
+  // 点击重置密码
+  const resetPassFu = useCallback(async (id: number) => {
+    await userApi.resetPwd(id);
+    message.open({
+      type: "success",
+      content: "操作成功",
+    });
+  }, []);
+
+  const openEditPageFu = useCallback(
+    (id: number) => {
+      if (id === 0 && total >= 50)
+        return message.open({
+          type: "warning",
+          content: "最多支持50个用户!",
+        });
+
+      editId.current = id;
+      setEditPageShow(true);
+    },
+    [total]
+  );
+
+  const COLUMNS = useMemo(() => {
+    return [
+      {
+        title: "账号名",
+        dataIndex: "userName",
+      },
+      {
+        title: "用户昵称",
+        dataIndex: "nickName",
+      },
+      {
+        title: "真实姓名",
+        dataIndex: "realName",
+      },
+      {
+        title: "注册时间",
+        dataIndex: "createTime",
+      },
+
+      {
+        title: "启用状态",
+        render: (item: UserTableListType) => (
+          <Switch
+            disabled={item.isAdmin === 1}
+            checkedChildren="启用"
+            unCheckedChildren="停用"
+            checked={item.isEnabled === 1}
+            onChange={(val) => isEnabledClickFu(val, item.id)}
+          />
+        ),
+      },
+
+      {
+        title: "操作",
+        render: (item: UserTableListType) => {
+          return item.isAdmin === 1 ? (
+            "-"
+          ) : (
+            <DageTableActions
+              renderBefore={
+                <Popconfirm
+                  title="密码重制后为123456,是否重置?"
+                  okText="重置"
+                  cancelText="取消"
+                  onConfirm={() => resetPassFu(item.id)}
+                >
+                  <Button size="small" type="text">
+                    重置密码
+                  </Button>
+                </Popconfirm>
+              }
+              onEdit={openEditPageFu.bind(undefined, item.id)}
+              onDelete={delTableFu.bind(undefined, item.id)}
+            />
+          );
+        },
+      },
+    ];
+  }, [delTableFu, isEnabledClickFu, resetPassFu, openEditPageFu]);
+
+  return (
+    <div className="user">
+      <Form ref={formRef} layout="inline" onValuesChange={debounceSearch}>
+        <Form.Item label="账号" name="searchKey">
+          <Input
+            className="w220"
+            placeholder="请输入关键字"
+            maxLength={30}
+            showCount
+            allowClear
+          />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" onClick={openEditPageFu.bind(undefined, 0)}>
+            新增
+          </Button>
+        </Form.Item>
+        <Form.Item>
+          <Button onClick={handleReset}>重置</Button>
+        </Form.Item>
+      </Form>
+
+      <Table
+        loading={loading}
+        className="page-table"
+        dataSource={list}
+        columns={COLUMNS}
+        rowKey="id"
+        pagination={{
+          showQuickJumper: true,
+          position: ["bottomCenter"],
+          showSizeChanger: true,
+          current: params.pageNum,
+          pageSize: params.pageSize,
+          total,
+          onChange: paginationChange(),
+        }}
+      />
+
+      {/* 点击新增或者编辑 */}
+      {editPageShow ? (
+        <UserAdd
+          id={editId.current}
+          closePage={() => setEditPageShow(false)}
+          upTableList={getList}
+          addTableList={handleReset}
+        />
+      ) : null}
+    </div>
+  );
+}

+ 40 - 0
src/router/index.tsx

@@ -0,0 +1,40 @@
+import { SettingOutlined } from "@ant-design/icons";
+import React from "react";
+import { DageRouteItem } from "./types";
+
+export const DEFAULT_MENU: DageRouteItem[] = [
+  {
+    path: "/assessment",
+    title: "考核设置",
+    icon: <SettingOutlined />,
+    children: [
+      {
+        path: "/index",
+        title: "指标设置",
+        Component: React.lazy(() => import("../pages/Assessment/Index")),
+      },
+    ],
+  },
+];
+
+export const DEFAULT_ADMIN_MENU: DageRouteItem[] = [
+  {
+    path: "/setting",
+    title: "系统设置",
+    icon: <SettingOutlined />,
+    children: [
+      {
+        path: "/user",
+        title: "用户管理",
+        Component: React.lazy(() => import("../pages/User")),
+      },
+      {
+        path: "/log",
+        title: "操作日志",
+        Component: React.lazy(() => import("../pages/Log")),
+      },
+    ],
+  },
+];
+
+export * from "./types";

+ 16 - 0
src/router/types.ts

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

+ 20 - 0
src/store/index.ts

@@ -0,0 +1,20 @@
+// 导入 redux
+import { applyMiddleware, legacy_createStore as createStore } from 'redux'
+// 导入自己封装的  rootReducer 
+import rootReducer from './reducer'
+// 导入调试工具和 异步的 redux(用来发送异步请求)
+// 调试工具需要下载谷歌 扩展程序 我用的是 Redux DevTools 3.0.17
+import { composeWithDevTools } from 'redux-devtools-extension'
+import thunk from 'redux-thunk'
+
+// 创建仓库实例
+const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)))
+
+// 声明 RootState,在使用仓库的时候用来使用
+export type RootState = ReturnType<typeof store.getState>
+
+// 声明 AppDispatch,在异步请求的时候来使用
+export type AppDispatch = typeof store.dispatch
+
+// 导出仓库实例
+export default store

+ 22 - 0
src/store/reducer/base.ts

@@ -0,0 +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;
+  }
+}

+ 11 - 0
src/store/reducer/index.ts

@@ -0,0 +1,11 @@
+// 导入合并reducer的依赖
+import { combineReducers } from "redux";
+import base from "./base";
+
+// 合并 reducer
+const rootReducer = combineReducers({
+  base,
+});
+
+// 默认导出
+export default rootReducer;

+ 6 - 0
src/theme.scss

@@ -0,0 +1,6 @@
+$primaryColor: #4284ff;
+$secondColor: #6bc6ff;
+
+:export {
+  primaryColor: $primaryColor;
+}

+ 30 - 0
src/types/index.ts

@@ -0,0 +1,30 @@
+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";

+ 7 - 0
src/types/log.ts

@@ -0,0 +1,7 @@
+export interface GetLogListParams {
+  startTime?: string;
+  endTime?: string;
+  searchKey?: string;
+  pageSize: number;
+  pageNum: number;
+}

+ 31 - 0
src/types/user.ts

@@ -0,0 +1,31 @@
+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;
+};

+ 9 - 0
src/utils/index.ts

@@ -0,0 +1,9 @@
+import { removeTokenInfo } from "@dage/pc-components";
+import { logoutApi } from "@/api";
+
+export const logout = async () => {
+  await logoutApi();
+
+  removeTokenInfo();
+  globalThis.location.href = "#/login";
+};

+ 23 - 0
tsconfig.json

@@ -0,0 +1,23 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "noFallthroughCasesInSwitch": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "include": ["src"]
+}