Browse Source

main代码合并到electron-main (#1834)

* upgrade electron version:11->18.

upgrade electron version:11->18.
Packaging failed because the original electronic version was too old.
将electron 11 改成18.因为11太老导致打包错误。

* main->electron-main合并代码

* main->electron-main合并代码

* main->electron-main合并代码
sanmen359 2 years ago
parent
commit
de231901a6
100 changed files with 1525 additions and 608 deletions
  1. 4 6
      .eslintrc.js
  2. 2 2
      .gitpod.yml
  3. 0 8
      .husky/lintstagedrc.js
  4. 0 2
      .husky/pre-commit
  5. 1 1
      .vscode/extensions.json
  6. 1 1
      .vscode/launch.json
  7. 7 23
      .vscode/settings.json
  8. 0 48
      .yarnclean
  9. 34 0
      CHANGELOG.en_US.md
  10. 1 1
      CHANGELOG.md
  11. 35 0
      CHANGELOG.zh_CN.md
  12. 3 3
      README.md
  13. 3 3
      README.zh-CN.md
  14. 2 2
      build/generate/icon/index.ts
  15. 14 13
      build/script/buildConf.ts
  16. 1 1
      build/script/compilerElectron.ts
  17. 3 3
      build/script/postBuild.ts
  18. 2 2
      build/utils.ts
  19. 0 21
      build/vite/optimizer.ts
  20. 3 3
      build/vite/plugin/compress.ts
  21. 0 25
      build/vite/plugin/hmr.ts
  22. 3 3
      build/vite/plugin/html.ts
  23. 8 8
      build/vite/plugin/index.ts
  24. 63 7
      build/vite/plugin/styleImport.ts
  25. 2 2
      build/vite/plugin/svgSprite.ts
  26. 3 3
      build/vite/plugin/theme.ts
  27. 1 1
      build/vite/proxy.ts
  28. 1 0
      commitlint.config.js
  29. 1 1
      electron-main/index.ts
  30. 7 14
      index.html
  31. 0 36
      jest.config.mjs
  32. 9 0
      mock/demo/account.ts
  33. 325 0
      mock/demo/api-cascader.ts
  34. 4 4
      mock/demo/select-demo.ts
  35. 1 1
      mock/demo/table-demo.ts
  36. 8 0
      mock/sys/user.ts
  37. 38 29
      package.json
  38. 0 8
      prettier.config.js
  39. 1 0
      src/App.vue
  40. 3 0
      src/api/demo/account.ts
  41. 9 0
      src/api/demo/cascader.ts
  42. 12 0
      src/api/demo/model/areaModel.ts
  43. 1 0
      src/api/demo/table.ts
  44. 1 1
      src/api/model/baseModel.ts
  45. 14 0
      src/api/sys/user.ts
  46. 1 1
      src/components/Application/src/AppDarkModeToggle.vue
  47. 2 2
      src/components/Application/src/AppLocalePicker.vue
  48. 1 0
      src/components/Application/src/AppLogo.vue
  49. 1 1
      src/components/Application/src/search/AppSearchFooter.vue
  50. 7 7
      src/components/Application/src/search/AppSearchModal.vue
  51. 4 5
      src/components/CardList/src/CardList.vue
  52. 1 1
      src/components/CardList/src/data.ts
  53. 2 0
      src/components/CodeEditor/index.ts
  54. 9 9
      src/components/CodeEditor/src/CodeEditor.vue
  55. 9 1
      src/components/CodeEditor/src/codemirror/CodeMirror.vue
  56. 7 21
      src/components/CodeEditor/src/codemirror/codemirror.css
  57. 5 0
      src/components/CodeEditor/src/typing.ts
  58. 8 3
      src/components/Container/src/collapse/CollapseContainer.vue
  59. 15 14
      src/components/ContextMenu/src/ContextMenu.vue
  60. 4 4
      src/components/Cropper/src/CopperModal.vue
  61. 4 5
      src/components/Cropper/src/CropperAvatar.vue
  62. 4 1
      src/components/Description/src/Description.vue
  63. 1 1
      src/components/Drawer/src/BasicDrawer.vue
  64. 1 2
      src/components/Drawer/src/typing.ts
  65. 55 66
      src/components/Dropdown/src/Dropdown.vue
  66. 1 1
      src/components/Excel/src/Export2Excel.ts
  67. 35 4
      src/components/Excel/src/ImportExcel.vue
  68. 3 0
      src/components/Form/index.ts
  69. 12 2
      src/components/Form/src/BasicForm.vue
  70. 6 0
      src/components/Form/src/componentMap.ts
  71. 198 0
      src/components/Form/src/components/ApiCascader.vue
  72. 130 0
      src/components/Form/src/components/ApiRadioGroup.vue
  73. 12 7
      src/components/Form/src/components/ApiSelect.vue
  74. 90 0
      src/components/Form/src/components/ApiTree.vue
  75. 1 1
      src/components/Form/src/components/ApiTreeSelect.vue
  76. 20 8
      src/components/Form/src/components/FormItem.vue
  77. 2 0
      src/components/Form/src/helper.ts
  78. 37 9
      src/components/Form/src/hooks/useFormEvents.ts
  79. 47 3
      src/components/Form/src/hooks/useFormValues.ts
  80. 6 3
      src/components/Form/src/hooks/useLabelWidth.ts
  81. 2 1
      src/components/Form/src/props.ts
  82. 6 3
      src/components/Form/src/types/form.ts
  83. 3 0
      src/components/Form/src/types/index.ts
  84. 73 98
      src/components/Icon/src/IconPicker.vue
  85. 15 3
      src/components/Loading/src/Loading.vue
  86. 2 1
      src/components/Markdown/src/MarkdownViewer.vue
  87. 4 2
      src/components/Menu/src/BasicMenu.vue
  88. 5 2
      src/components/Modal/src/BasicModal.vue
  89. 7 2
      src/components/Modal/src/components/Modal.tsx
  90. 1 1
      src/components/Modal/src/components/ModalClose.vue
  91. 1 0
      src/components/Modal/src/components/ModalHeader.vue
  92. 4 1
      src/components/Modal/src/index.less
  93. 2 2
      src/components/Page/src/PageFooter.vue
  94. 6 1
      src/components/Page/src/PageWrapper.vue
  95. 7 7
      src/components/Preview/src/Functional.vue
  96. 1 1
      src/components/Preview/src/Preview.vue
  97. 5 6
      src/components/Scrollbar/src/Scrollbar.vue
  98. 1 1
      src/components/SimpleMenu/src/components/SubMenuItem.vue
  99. 7 7
      src/components/SimpleMenu/src/components/menu.less
  100. 1 1
      src/components/StrengthMeter/src/StrengthMeter.vue

+ 4 - 6
.eslintrc.js

@@ -1,6 +1,4 @@
-// @ts-check
-const { defineConfig } = require('eslint-define-config');
-module.exports = defineConfig({
+module.exports = {
   root: true,
   env: {
     browser: true,
@@ -20,9 +18,7 @@ module.exports = defineConfig({
   extends: [
     'plugin:vue/vue3-recommended',
     'plugin:@typescript-eslint/recommended',
-    'prettier',
     'plugin:prettier/recommended',
-    'plugin:jest/recommended',
   ],
   rules: {
     'vue/script-setup-uses-vars': 'error',
@@ -62,6 +58,7 @@ module.exports = defineConfig({
     'vue/singleline-html-element-content-newline': 'off',
     'vue/attribute-hyphenation': 'off',
     'vue/require-default-prop': 'off',
+    'vue/require-explicit-emits': 'off',
     'vue/html-self-closing': [
       'error',
       {
@@ -74,5 +71,6 @@ module.exports = defineConfig({
         math: 'always',
       },
     ],
+    'vue/multi-word-component-names': 'off',
   },
-});
+};

+ 2 - 2
.gitpod.yml

@@ -2,5 +2,5 @@ ports:
   - port: 3344
     onOpen: open-preview
 tasks:
-  - init: yarn
-    command: yarn dev
+  - init: pnpm install
+    command: pnpm run dev

+ 0 - 8
.husky/lintstagedrc.js

@@ -1,8 +0,0 @@
-module.exports = {
-  '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
-  '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],
-  'package.json': ['prettier --write'],
-  '*.vue': ['eslint --fix', 'prettier --write', 'stylelint --fix'],
-  '*.{scss,less,styl,html}': ['stylelint --fix', 'prettier --write'],
-  '*.md': ['prettier --write'],
-};

+ 0 - 2
.husky/pre-commit

@@ -6,5 +6,3 @@
 
 # Format and submit code according to lintstagedrc.js configuration
 npm run lint:lint-staged
-
-npm run lint:pretty

+ 1 - 1
.vscode/extensions.json

@@ -1,6 +1,6 @@
 {
   "recommendations": [
-    "octref.vetur",
+    "johnsoncodehk.volar",
     "dbaeumer.vscode-eslint",
     "stylelint.vscode-stylelint",
     "esbenp.prettier-vscode",

+ 1 - 1
.vscode/launch.json

@@ -5,7 +5,7 @@
       "type": "chrome",
       "request": "launch",
       "name": "Launch Chrome",
-      "url": "http://localhost:3100",
+      "url": "https://localhost:3100",
       "webRoot": "${workspaceFolder}/src",
       "sourceMaps": true
     }

+ 7 - 23
.vscode/settings.json

@@ -1,23 +1,10 @@
 {
   "typescript.tsdk": "./node_modules/typescript/lib",
-  "typescript.enablePromptUseWorkspaceTsdk": true,
   "volar.tsPlugin": true,
   "volar.tsPluginStatus": false,
-  //===========================================
-  //============= Editor ======================
-  //===========================================
-  "explorer.openEditors.visible": 0,
+  "npm.packageManager": "pnpm",
   "editor.tabSize": 2,
   "editor.defaultFormatter": "esbenp.prettier-vscode",
-  "diffEditor.ignoreTrimWhitespace": false,
-  //===========================================
-  //============= Other =======================
-  //===========================================
-  "breadcrumbs.enabled": true,
-  "open-in-browser.default": "chrome",
-  //===========================================
-  //============= files =======================
-  //===========================================
   "files.eol": "\n",
   "search.exclude": {
     "**/node_modules": true,
@@ -68,16 +55,10 @@
     "**/yarn.lock": true
   },
   "stylelint.enable": true,
-  "stylelint.packageManager": "yarn",
-  "liveServer.settings.donotShowInfoMsg": true,
-  "telemetry.enableCrashReporter": false,
-  "workbench.settings.enableNaturalLanguageSearch": false,
+  "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"],
   "path-intellisense.mappings": {
     "/@/": "${workspaceRoot}/src"
   },
-  "prettier.requireConfig": true,
-  "typescript.updateImportsOnFileMove.enabled": "always",
-  "workbench.sideBar.location": "left",
   "[javascriptreact]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
@@ -107,7 +88,8 @@
   },
   "[vue]": {
     "editor.codeActionsOnSave": {
-      "source.fixAll.eslint": false
+      "source.fixAll.eslint": true,
+      "source.fixAll.stylelint": true
     }
   },
   "i18n-ally.localesPaths": ["src/locales/lang"],
@@ -150,6 +132,8 @@
     "lintstagedrc",
     "brotli",
     "tailwindcss",
-    "sider"
+    "sider",
+    "pnpm",
+    "antd"
   ]
 }

+ 0 - 48
.yarnclean

@@ -1,48 +0,0 @@
-# test directories
-__tests__
-test
-tests
-powered-test
-
-# asset directories
-docs
-doc
-website
-images
-assets
-
-# examples
-example
-examples
-
-# code coverage directories
-coverage
-.nyc_output
-
-# build scripts
-Makefile
-Gulpfile.js
-Gruntfile.js
-
-# configs
-appveyor.yml
-circle.yml
-codeship-services.yml
-codeship-steps.yml
-wercker.yml
-.tern-project
-.gitattributes
-.editorconfig
-.*ignore
-.eslintrc
-.jshintrc
-.flowconfig
-.documentup.json
-.yarn-metadata.json
-.travis.yml
-
-# misc
-*.md
-
-!istanbul-reports/lib/html/assets
-!istanbul-api/node_modules/istanbul-reports/lib/html/assets

+ 34 - 0
CHANGELOG.en_US.md

@@ -1,3 +1,37 @@
+## 2.8.0(2021-11.03)
+
+### Upgrade Instructions
+
+- Package manager changed from `yarn` to `pnpm`
+- Delete `node_modules` and `yarn.lock`, install `pnpm` globally
+- Execute `pnpm install`
+
+### ✨ Features
+
+- **Others**
+  - The `VITE_PROXY` configuration in the `.env` file supports single quotes
+  - Remove warnings during build
+
+### 🐛 Bug Fixes
+
+- **BasicTable**
+  - Fix the issue that editable cells cannot be submitted in some cases
+  - Fix the problem that the `inset` attribute does not work
+  - Fix the problem that the performance of `useTable` and `reload` method `await` of `BasicTable` instance are inconsistent
+  - Fix the issue that `clickToRowSelect` would ignore the disabled state of the row selection box
+  - Fix the problem that the page of `BasicTable` will be reset in some cases
+  - Modify the `deleteTableDataRecord` method
+- **BasicModal**
+  - Fixed the problem that `Modal` could not be closed even when clicking on the mask and pressing the `Esc` key
+  - Fixed the issue that clicking the close button and the blank area next to the maximize button would also cause `Modal` to close
+- **BasicTree** Fix the problem that the node slot does not work
+- **CodeEditor** Fix the problem that may cause `Build` failure
+- **BasicForm** Fix the problem that the content width of the custom FormItem component may be out of range
+- **ApiTreeSelect** Fix the problem that the change of `params` failed to trigger the re-request of api data
+- **Others** -Fixed an issue where multiple tabs would not jump to routing when closing tabs in some cases
+  - Fix the issue that some components may cause abnormal hot update
+  - Fix the problem that some sub-components of `antdv` will be reported in the build process when directly `import` part of the `antdv`, such as: TabPane, RadioGroup
+
 ## 2.7.2(2021-09-14)
 
 ### ✨ Features

+ 1 - 1
CHANGELOG.md

@@ -1,4 +1,4 @@
-## [2.7.2](https://github.com/anncwb/vue-vben-admin/compare/v2.7.1...v2.7.2) (2021-09-13)
+## [2.8.0](https://github.com/anncwb/vue-vben-admin/compare/v2.7.2...v2.8.0) (2021-11-03)
 
 ### Bug Fixes
 

+ 35 - 0
CHANGELOG.zh_CN.md

@@ -1,3 +1,38 @@
+## 2.8.0(2021-11.03)
+
+### 升级说明
+
+- 包管理器由`yarn`改为 `pnpm`
+- 删除`node_modules`和`yarn.lock`,全局安装`pnpm`
+- 执行`pnpm install`
+
+### ✨ Features
+
+- **其它**
+  - `.env`文件中的`VITE_PROXY`配置支持单引号
+  - 移除 build 过程中的警告
+
+### 🐛 Bug Fixes
+
+- **BasicTable**
+  - 修复可编辑单元格某些情况下无法提交的问题
+  - 修复`inset`属性不起作用的问题
+  - 修复`useTable`与`BasicTable`实例的`reload`方法`await`表现不一致的问题
+  - 修复`clickToRowSelect`会无视行选择框 disabled 状态的问题
+  - 修复`BasicTable`在某些情况下,分页会被重置的问题
+  - 修改 `deleteTableDataRecord` 方法
+- **BasicModal**
+  - 修复点击遮罩、按下`Esc`键都不能关闭`Modal`的问题
+  - 修复点击关闭按钮、最大化按钮旁边的空白区域也会导致`Modal`关闭的问题
+- **BasicTree** 修复节点插槽不起作用的问题
+- **CodeEditor** 修复可能会造成的`Build`失败的问题
+- **BasicForm** 修复自定义 FormItem 组件的内容宽度可能超出范围的问题
+- **ApiTreeSelect** 修复`params`变化未能触发重新请求 api 数据的问题
+- **其它**
+  - 修复多标签在某些情况下关闭页签不会跳转路由的问题
+  - 修复部分组件可能会造成热更新异常的问题
+  - 修复直接`import`部分`antdv`子组件时会在 build 过程中报错的问题,如:TabPane、RadioGroup
+
 ## 2.7.2(2021-09-14)
 
 ### ✨ Features

+ 3 - 3
README.md

@@ -70,20 +70,20 @@ git clone https://github.com/anncwb/vue-vben-admin.git
 ```bash
 cd vue-vben-admin
 
-yarn install
+pnpm install
 
 ```
 
 - run
 
 ```bash
-yarn serve
+pnpm serve
 ```
 
 - build
 
 ```bash
-yarn build
+pnpm build
 ```
 
 ## Change Log

+ 3 - 3
README.zh-CN.md

@@ -70,20 +70,20 @@ git clone https://github.com/anncwb/vue-vben-admin.git
 ```bash
 cd vue-vben-admin
 
-yarn install
+pnpm install
 
 ```
 
 - 运行
 
 ```bash
-yarn serve
+pnpm serve
 ```
 
 - 打包
 
 ```bash
-yarn build
+pnpm build
 ```
 
 ## 更新日志

+ 2 - 2
build/generate/icon/index.ts

@@ -1,7 +1,7 @@
 import path from 'path';
 import fs from 'fs-extra';
 import inquirer from 'inquirer';
-import chalk from 'chalk';
+import colors from 'picocolors';
 import pkg from '../../../package.json';
 
 async function generateIcon() {
@@ -64,7 +64,7 @@ async function generateIcon() {
       }
       fs.emptyDir(path.join(process.cwd(), 'node_modules/.vite'));
       console.log(
-        `✨ ${chalk.cyan(`[${pkg.name}]`)}` + ' - Icon generated successfully:' + `[${prefixSet}]`,
+        `✨ ${colors.cyan(`[${pkg.name}]`)}` + ' - Icon generated successfully:' + `[${prefixSet}]`,
       );
     });
 }

+ 14 - 13
build/script/buildConf.ts

@@ -3,20 +3,21 @@
  */
 import { GLOB_CONFIG_FILE_NAME, OUTPUT_DIR } from '../constant';
 import fs, { writeFileSync } from 'fs-extra';
-import chalk from 'chalk';
+import colors from 'picocolors';
 
-import { getRootPath, getEnvConfig } from '../utils';
+import { getEnvConfig, getRootPath } from '../utils';
 import { getConfigFileName } from '../getConfigFileName';
 
 import pkg from '../../package.json';
 
-function createConfig(
-  {
-    configName,
-    config,
-    configFileName = GLOB_CONFIG_FILE_NAME,
-  }: { configName: string; config: any; configFileName?: string } = { configName: '', config: {} },
-) {
+interface CreateConfigParams {
+  configName: string;
+  config: any;
+  configFileName?: string;
+}
+
+function createConfig(params: CreateConfigParams) {
+  const { configName, config, configFileName } = params;
   try {
     const windowConf = `window.${configName}`;
     // Ensure that the variable will not be modified
@@ -30,15 +31,15 @@ function createConfig(
     fs.mkdirp(getRootPath(OUTPUT_DIR));
     writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr);
 
-    console.log(chalk.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`);
-    console.log(chalk.gray(OUTPUT_DIR + '/' + chalk.green(configFileName)) + '\n');
+    console.log(colors.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`);
+    console.log(colors.gray(OUTPUT_DIR + '/' + colors.green(configFileName)) + '\n');
   } catch (error) {
-    console.log(chalk.red('configuration file configuration file failed to package:\n' + error));
+    console.log(colors.red('configuration file configuration file failed to package:\n' + error));
   }
 }
 
 export function runBuildConfig() {
   const config = getEnvConfig();
   const configFileName = getConfigFileName(config);
-  createConfig({ config, configName: configFileName });
+  createConfig({ config, configName: configFileName, configFileName: GLOB_CONFIG_FILE_NAME });
 }

+ 1 - 1
build/script/compilerElectron.ts

@@ -14,7 +14,7 @@ const TAG = '[compiler-electron]';
 
 export function startCompilerElectron(port = 80) {
   // 因为 vite 不会重定向到 index.html,所以直接写 index.html 路由。
-  const ELECTRON_URL = `http://localhost:${port}/index.html`;
+  const ELECTRON_URL = `https://localhost:${port}/index.html`;
 
   const spinner = ora(`${TAG} Electron build...`);
   const electron = electronConnect.server.create({ stopOnClose: true });

+ 3 - 3
build/script/postBuild.ts

@@ -1,7 +1,7 @@
 // #!/usr/bin/env node
 
 import { runBuildConfig } from './buildConf';
-import chalk from 'chalk';
+import colors from 'picocolors';
 
 import pkg from '../../package.json';
 
@@ -14,9 +14,9 @@ export const runBuild = async () => {
       runBuildConfig();
     }
 
-    console.log(`✨ ${chalk.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
+    console.log(`✨ ${colors.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
   } catch (error) {
-    console.log(chalk.red('vite build error:\n' + error));
+    console.log(colors.red('vite build error:\n' + error));
     process.exit(1);
   }
 };

+ 2 - 2
build/utils.ts

@@ -28,9 +28,9 @@ export function wrapperEnv(envConf: Recordable): ViteEnv {
     if (envName === 'VITE_PORT') {
       realName = Number(realName);
     }
-    if (envName === 'VITE_PROXY') {
+    if (envName === 'VITE_PROXY' && realName) {
       try {
-        realName = JSON.parse(realName);
+        realName = JSON.parse(realName.replace(/'/g, '"'));
       } catch (error) {
         realName = '';
       }

+ 0 - 21
build/vite/optimizer.ts

@@ -1,21 +0,0 @@
-// TODO
-import type { GetManualChunk } from 'rollup';
-
-//
-const vendorLibs: { match: string[]; output: string }[] = [
-  // {
-  //   match: ['xlsx'],
-  //   output: 'xlsx',
-  // },
-];
-
-// @ts-ignore
-export const configManualChunk: GetManualChunk = (id: string) => {
-  if (/[\\/]node_modules[\\/]/.test(id)) {
-    const matchItem = vendorLibs.find((item) => {
-      const reg = new RegExp(`[\\/]node_modules[\\/]_?(${item.match.join('|')})(.*)`, 'ig');
-      return reg.test(id);
-    });
-    return matchItem ? matchItem.output : null;
-  }
-};

+ 3 - 3
build/vite/plugin/compress.ts

@@ -2,16 +2,16 @@
  * Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
  * https://github.com/anncwb/vite-plugin-compression
  */
-import type { Plugin } from 'vite';
+import type { PluginOption } from 'vite';
 import compressPlugin from 'vite-plugin-compression';
 
 export function configCompressPlugin(
   compress: 'gzip' | 'brotli' | 'none',
   deleteOriginFile = false,
-): Plugin | Plugin[] {
+): PluginOption | PluginOption[] {
   const compressList = compress.split(',');
 
-  const plugins: Plugin[] = [];
+  const plugins: PluginOption[] = [];
 
   if (compressList.includes('gzip')) {
     plugins.push(

+ 0 - 25
build/vite/plugin/hmr.ts

@@ -1,25 +0,0 @@
-import type { Plugin } from 'vite';
-
-/**
- * TODO
- * Temporarily solve the Vite circular dependency problem, and wait for a better solution to fix it later. I don't know what problems this writing will bring.
- * @returns
- */
-
-export function configHmrPlugin(): Plugin {
-  return {
-    name: 'singleHMR',
-    handleHotUpdate({ modules, file }) {
-      if (file.match(/xml$/)) return [];
-
-      modules.forEach((m) => {
-        if (!m.url.match(/\.(css|less)/)) {
-          m.importedModules = new Set();
-          m.importers = new Set();
-        }
-      });
-
-      return modules;
-    },
-  };
-}

+ 3 - 3
build/vite/plugin/html.ts

@@ -2,8 +2,8 @@
  * Plugin to minimize and use ejs template syntax in index.html.
  * https://github.com/anncwb/vite-plugin-html
  */
-import type { Plugin } from 'vite';
-import html from 'vite-plugin-html';
+import type { PluginOption } from 'vite';
+import { createHtmlPlugin } from 'vite-plugin-html';
 import pkg from '../../../package.json';
 import { GLOB_CONFIG_FILE_NAME } from '../../constant';
 
@@ -16,7 +16,7 @@ export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
     return `${path || '/'}${GLOB_CONFIG_FILE_NAME}?v=${pkg.version}-${new Date().getTime()}`;
   };
 
-  const htmlPlugin: Plugin[] = html({
+  const htmlPlugin: PluginOption[] = createHtmlPlugin({
     minify: isBuild,
     inject: {
       // Inject data into ejs template

+ 8 - 8
build/vite/plugin/index.ts

@@ -1,9 +1,10 @@
-import type { Plugin } from 'vite';
+import { PluginOption } from 'vite';
 import vue from '@vitejs/plugin-vue';
 import vueJsx from '@vitejs/plugin-vue-jsx';
 import legacy from '@vitejs/plugin-legacy';
 import purgeIcons from 'vite-plugin-purge-icons';
 import windiCSS from 'vite-plugin-windicss';
+import VitePluginCertificate from 'vite-plugin-mkcert';
 import vueSetupExtend from 'vite-plugin-vue-setup-extend';
 import { configHtmlPlugin } from './html';
 import { configPwaConfig } from './pwa';
@@ -14,7 +15,6 @@ import { configVisualizerConfig } from './visualizer';
 import { configThemePlugin } from './theme';
 import { configImageminPlugin } from './imagemin';
 import { configSvgIconsPlugin } from './svgSprite';
-import { configHmrPlugin } from './hmr';
 
 export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
   const {
@@ -25,21 +25,21 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
     VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE,
   } = viteEnv;
 
-  const vitePlugins: (Plugin | Plugin[])[] = [
+  const vitePlugins: (PluginOption | PluginOption[])[] = [
     // have to
     vue(),
     // have to
     vueJsx(),
     // support name
     vueSetupExtend(),
+    VitePluginCertificate({
+      source: 'coding',
+    }),
   ];
 
   // vite-plugin-windicss
   vitePlugins.push(windiCSS());
 
-  // TODO
-  !isBuild && vitePlugins.push(configHmrPlugin());
-
   // @vitejs/plugin-legacy
   VITE_LEGACY && isBuild && vitePlugins.push(legacy());
 
@@ -61,12 +61,12 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
   // rollup-plugin-visualizer
   vitePlugins.push(configVisualizerConfig());
 
-  //vite-plugin-theme
+  // vite-plugin-theme
   vitePlugins.push(configThemePlugin(isBuild));
 
   // The following plugins only work in the production environment
   if (isBuild) {
-    //vite-plugin-imagemin
+    // vite-plugin-imagemin
     VITE_USE_IMAGEMIN && vitePlugins.push(configImageminPlugin());
 
     // rollup-plugin-gzip

+ 63 - 7
build/vite/plugin/styleImport.ts

@@ -2,19 +2,75 @@
  *  Introduces component library styles on demand.
  * https://github.com/anncwb/vite-plugin-style-import
  */
-import styleImport from 'vite-plugin-style-import';
+import { createStyleImportPlugin } from 'vite-plugin-style-import';
 
-export function configStyleImportPlugin(isBuild: boolean) {
-  if (!isBuild) {
-    return [];
-  }
-  const styleImportPlugin = styleImport({
+export function configStyleImportPlugin(_isBuild: boolean) {
+  // if (!isBuild) {
+  //   return [];
+  // }
+  const styleImportPlugin = createStyleImportPlugin({
     libs: [
       {
         libraryName: 'ant-design-vue',
         esModule: true,
         resolveStyle: (name) => {
-          return `ant-design-vue/es/${name}/style/index`;
+          // 这里是无需额外引入样式文件的“子组件”列表
+          const ignoreList = [
+            'anchor-link',
+            'sub-menu',
+            'menu-item',
+            'menu-divider',
+            'menu-item-group',
+            'breadcrumb-item',
+            'breadcrumb-separator',
+            'form-item',
+            'step',
+            'select-option',
+            'select-opt-group',
+            'card-grid',
+            'card-meta',
+            'collapse-panel',
+            'descriptions-item',
+            'list-item',
+            'list-item-meta',
+            'table-column',
+            'table-column-group',
+            'tab-pane',
+            'tab-content',
+            'timeline-item',
+            'tree-node',
+            'skeleton-input',
+            'skeleton-avatar',
+            'skeleton-title',
+            'skeleton-paragraph',
+            'skeleton-image',
+            'skeleton-button',
+          ];
+          // 这里是需要额外引入样式的子组件列表
+          // 单独引入子组件时需引入组件样式,否则会在打包后导致子组件样式丢失
+          const replaceList = {
+            'typography-text': 'typography',
+            'typography-title': 'typography',
+            'typography-paragraph': 'typography',
+            'typography-link': 'typography',
+            'dropdown-button': 'dropdown',
+            'input-password': 'input',
+            'input-search': 'input',
+            'input-group': 'input',
+            'radio-group': 'radio',
+            'checkbox-group': 'checkbox',
+            'layout-sider': 'layout',
+            'layout-content': 'layout',
+            'layout-footer': 'layout',
+            'layout-header': 'layout',
+            'month-picker': 'date-picker',
+          };
+
+          return ignoreList.includes(name)
+            ? ''
+            : replaceList.hasOwnProperty(name)
+            ? `ant-design-vue/es/${replaceList[name]}/style/index`
+            : `ant-design-vue/es/${name}/style/index`;
         },
       },
     ],

+ 2 - 2
build/vite/plugin/svgSprite.ts

@@ -3,11 +3,11 @@
  * https://github.com/anncwb/vite-plugin-svg-icons
  */
 
-import SvgIconsPlugin from 'vite-plugin-svg-icons';
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
 import path from 'path';
 
 export function configSvgIconsPlugin(isBuild: boolean) {
-  const svgIconsPlugin = SvgIconsPlugin({
+  const svgIconsPlugin = createSvgIconsPlugin({
     iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
     svgoOptions: isBuild,
     // default

+ 3 - 3
build/vite/plugin/theme.ts

@@ -2,7 +2,7 @@
  * Vite plugin for website theme color switching
  * https://github.com/anncwb/vite-plugin-theme
  */
-import type { Plugin } from 'vite';
+import type { PluginOption } from 'vite';
 import path from 'path';
 import {
   viteThemePlugin,
@@ -14,7 +14,7 @@ import {
 import { getThemeColors, generateColors } from '../../config/themeConfig';
 import { generateModifyVars } from '../../generate/generateModifyVars';
 
-export function configThemePlugin(isBuild: boolean): Plugin[] {
+export function configThemePlugin(isBuild: boolean): PluginOption[] {
   const colors = generateColors({
     mixDarken,
     mixLighten,
@@ -85,5 +85,5 @@ export function configThemePlugin(isBuild: boolean): Plugin[] {
     }),
   ];
 
-  return plugin as unknown as Plugin[];
+  return plugin as unknown as PluginOption[];
 }

+ 1 - 1
build/vite/proxy.ts

@@ -7,7 +7,7 @@ type ProxyItem = [string, string];
 
 type ProxyList = ProxyItem[];
 
-type ProxyTargetList = Record<string, ProxyOptions & { rewrite: (path: string) => string }>;
+type ProxyTargetList = Record<string, ProxyOptions>;
 
 const httpsRE = /^https:\/\//;
 

+ 1 - 0
commitlint.config.js

@@ -7,6 +7,7 @@ module.exports = {
     'header-max-length': [2, 'always', 108],
     'subject-empty': [2, 'never'],
     'type-empty': [2, 'never'],
+    'subject-case': [0],
     'type-enum': [
       2,
       'always',

+ 1 - 1
electron-main/index.ts

@@ -26,7 +26,7 @@ class createWin {
       backgroundColor: '#fff',
     });
     const URL = is_dev
-      ? `http://localhost:${process.env.PORT}` // vite 启动的服务器地址
+      ? `https://localhost:${process.env.PORT}` // vite 启动的服务器地址
       : `file://${join(__dirname, '../index.html')}`; // vite 构建后的静态文件地址
 
     mainWindow.loadURL(URL);

+ 7 - 14
index.html

@@ -8,7 +8,6 @@
       name="viewport"
       content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
     />
-
     <title><%= title %></title>
     <link rel="icon" href="/favicon.ico" />
   </head>
@@ -30,7 +29,7 @@
         }
 
         html[data-theme='dark'] .app-loading .app-loading-title {
-          color: rgba(255, 255, 255, 0.85);
+          color: rgb(255 255 255 / 85%);
         }
 
         .app-loading {
@@ -48,7 +47,6 @@
           top: 50%;
           left: 50%;
           display: flex;
-          -webkit-transform: translate3d(-50%, -50%, 0);
           transform: translate3d(-50%, -50%, 0);
           justify-content: center;
           align-items: center;
@@ -66,7 +64,7 @@
           display: flex;
           margin-top: 30px;
           font-size: 30px;
-          color: rgba(0, 0, 0, 0.85);
+          color: rgb(0 0 0 / 85%);
           justify-content: center;
           align-items: center;
         }
@@ -97,7 +95,7 @@
           height: 20px;
           background-color: #0065cc;
           border-radius: 100%;
-          opacity: 0.3;
+          opacity: 30%;
           transform: scale(0.75);
           animation: antSpinMove 1s infinite linear alternate;
           transform-origin: 50% 50%;
@@ -111,43 +109,38 @@
         .dot i:nth-child(2) {
           top: 0;
           right: 0;
-          -webkit-animation-delay: 0.4s;
           animation-delay: 0.4s;
         }
 
         .dot i:nth-child(3) {
           right: 0;
           bottom: 0;
-          -webkit-animation-delay: 0.8s;
           animation-delay: 0.8s;
         }
 
         .dot i:nth-child(4) {
           bottom: 0;
           left: 0;
-          -webkit-animation-delay: 1.2s;
           animation-delay: 1.2s;
         }
         @keyframes antRotate {
           to {
-            -webkit-transform: rotate(405deg);
             transform: rotate(405deg);
           }
         }
-        @-webkit-keyframes antRotate {
+        @keyframes antRotate {
           to {
-            -webkit-transform: rotate(405deg);
             transform: rotate(405deg);
           }
         }
         @keyframes antSpinMove {
           to {
-            opacity: 1;
+            opacity: 100%;
           }
         }
-        @-webkit-keyframes antSpinMove {
+        @keyframes antSpinMove {
           to {
-            opacity: 1;
+            opacity: 100%;
           }
         }
       </style>

+ 0 - 36
jest.config.mjs

@@ -1,36 +0,0 @@
-export default {
-  preset: 'ts-jest',
-  roots: ['<rootDir>/tests/'],
-  clearMocks: true,
-  moduleDirectories: ['node_modules', 'src'],
-  moduleFileExtensions: ['js', 'ts', 'vue', 'tsx', 'jsx', 'json', 'node'],
-  modulePaths: ['<rootDir>/src', '<rootDir>/node_modules'],
-  testMatch: [
-    '**/tests/**/*.[jt]s?(x)',
-    '**/?(*.)+(spec|test).[tj]s?(x)',
-    '(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$',
-  ],
-  testPathIgnorePatterns: [
-    '<rootDir>/tests/server/',
-    '<rootDir>/tests/__mocks__/',
-    '/node_modules/',
-  ],
-  transform: {
-    '^.+\\.tsx?$': 'ts-jest',
-  },
-  transformIgnorePatterns: ['<rootDir>/tests/__mocks__/', '/node_modules/'],
-  // A map from regular expressions to module names that allow to stub out resources with a single module
-  moduleNameMapper: {
-    '\\.(vs|fs|vert|frag|glsl|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
-      '<rootDir>/tests/__mocks__/fileMock.ts',
-    '\\.(sass|s?css|less)$': '<rootDir>/tests/__mocks__/styleMock.ts',
-    '\\?worker$': '<rootDir>/tests/__mocks__/workerMock.ts',
-    '^/@/(.*)$': '<rootDir>/src/$1',
-  },
-  testEnvironment: 'jsdom',
-  verbose: true,
-  collectCoverage: false,
-  coverageDirectory: 'coverage',
-  collectCoverageFrom: ['src/**/*.{js,ts,vue}'],
-  coveragePathIgnorePatterns: ['^.+\\.d\\.ts$'],
-};

+ 9 - 0
mock/demo/account.ts

@@ -1,5 +1,6 @@
 import { MockMethod } from 'vite-plugin-mock';
 import { resultSuccess, resultError } from '../_util';
+import { ResultEnum } from '../../src/enums/httpEnum';
 
 const userInfo = {
   name: 'Vben',
@@ -59,4 +60,12 @@ export default [
       return resultError();
     },
   },
+  {
+    url: '/basic-api/user/tokenExpired',
+    method: 'post',
+    statusCode: 200,
+    response: () => {
+      return resultError('Token Expired!', { code: ResultEnum.TIMEOUT as number });
+    },
+  },
 ] as MockMethod[];

+ 325 - 0
mock/demo/api-cascader.ts

@@ -0,0 +1,325 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { resultSuccess } from '../_util';
+
+const areaList: any[] = [
+  {
+    id: '530825900854620160',
+    code: '430000',
+    parentCode: '100000',
+    levelType: 1,
+    name: '湖南省',
+    province: '湖南省',
+    city: null,
+    district: null,
+    town: null,
+    village: null,
+    parentPath: '430000',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 16:33:42',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530825900883980288',
+    code: '430100',
+    parentCode: '430000',
+    levelType: 2,
+    name: '长沙市',
+    province: '湖南省',
+    city: '长沙市',
+    district: null,
+    town: null,
+    village: null,
+    parentPath: '430000,430100',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 16:33:42',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530825900951089152',
+    code: '430102',
+    parentCode: '430100',
+    levelType: 3,
+    name: '芙蓉区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '芙蓉区',
+    town: null,
+    village: null,
+    parentPath: '430000,430100,430102',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 16:33:42',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530825901014003712',
+    code: '430104',
+    parentCode: '430100',
+    levelType: 3,
+    name: '岳麓区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '岳麓区',
+    town: null,
+    village: null,
+    parentPath: '430000,430100,430104',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 16:33:42',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530825900988837888',
+    code: '430103',
+    parentCode: '430100',
+    levelType: 3,
+    name: '天心区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: null,
+    village: null,
+    parentPath: '430000,430100,430103',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 16:33:42',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530826672489115648',
+    code: '430103002',
+    parentCode: '430103',
+    levelType: 4,
+    name: '坡子街街道',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: null,
+    parentPath: '430000,430100,430103,430103002',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-12-14 15:26:43',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241171607552',
+    code: '430103002001',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '八角亭社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '八角亭社区',
+    parentPath: '430000,430100,430103,430103002,430103002001',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2021-01-20 14:07:23',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241200967680',
+    code: '430103002002',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '西牌楼社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '西牌楼社区',
+    parentPath: '430000,430100,430103,430103002,430103002002',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 17:30:41',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241230327808',
+    code: '430103002003',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '太平街社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '太平街社区',
+    parentPath: '430000,430100,430103,430103002,430103002003',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 17:30:41',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241259687936',
+    code: '430103002005',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '坡子街社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '坡子街社区',
+    parentPath: '430000,430100,430103,430103002,430103002005',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 17:30:41',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241284853760',
+    code: '430103002006',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '青山祠社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '青山祠社区',
+    parentPath: '430000,430100,430103,430103002,430103002006',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 17:30:41',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241310019584',
+    code: '430103002007',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '沙河社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '沙河社区',
+    parentPath: '430000,430100,430103,430103002,430103002007',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 17:30:41',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241381322752',
+    code: '430103002008',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '碧湘社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '碧湘社区',
+    parentPath: '430000,430100,430103,430103002,430103002008',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 17:30:41',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241410682880',
+    code: '430103002009',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '创远社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '创远社区',
+    parentPath: '430000,430100,430103,430103002,430103002009',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 17:30:41',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241431654400',
+    code: '430103002010',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '楚湘社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '楚湘社区',
+    parentPath: '430000,430100,430103,430103002,430103002010',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 17:30:41',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241465208832',
+    code: '430103002011',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '西湖社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '西湖社区',
+    parentPath: '430000,430100,430103,430103002,430103002011',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 17:30:41',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241502957568',
+    code: '430103002012',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '登仁桥社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '登仁桥社区',
+    parentPath: '430000,430100,430103,430103002,430103002012',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 17:30:41',
+    customized: false,
+    usable: true,
+  },
+  {
+    id: '530840241553289216',
+    code: '430103002013',
+    parentCode: '430103002',
+    levelType: 5,
+    name: '文庙坪社区',
+    province: '湖南省',
+    city: '长沙市',
+    district: '天心区',
+    town: '坡子街街道',
+    village: '文庙坪社区',
+    parentPath: '430000,430100,430103,430103002,430103002013',
+    createTime: '2020-11-30 15:47:31',
+    updateTime: '2020-11-30 17:30:41',
+    customized: false,
+    usable: true,
+  },
+];
+export default [
+  {
+    url: '/basic-api/cascader/getAreaRecord',
+    timeout: 1000,
+    method: 'post',
+    response: ({ body }) => {
+      const { parentCode } = body || {};
+      if (!parentCode) {
+        return resultSuccess(areaList.filter((it) => it.code === '430000'));
+      }
+      return resultSuccess(areaList.filter((it) => it.parentCode === parentCode));
+    },
+  },
+] as MockMethod[];

+ 4 - 4
mock/demo/select-demo.ts

@@ -1,11 +1,11 @@
 import { MockMethod } from 'vite-plugin-mock';
 import { resultSuccess } from '../_util';
 
-const demoList = (keyword) => {
+const demoList = (keyword, count = 20) => {
   const result = {
     list: [] as any[],
   };
-  for (let index = 0; index < 20; index++) {
+  for (let index = 0; index < count; index++) {
     result.list.push({
       name: `${keyword ?? ''}选项${index}`,
       id: `${index}`,
@@ -20,9 +20,9 @@ export default [
     timeout: 1000,
     method: 'get',
     response: ({ query }) => {
-      const { keyword } = query;
+      const { keyword, count } = query;
       console.log(keyword);
-      return resultSuccess(demoList(keyword));
+      return resultSuccess(demoList(keyword, count));
     },
   },
 ] as MockMethod[];

+ 1 - 1
mock/demo/table-demo.ts

@@ -12,7 +12,7 @@ function getRandomPics(count = 10): string[] {
 
 const demoList = (() => {
   const result: any[] = [];
-  for (let index = 0; index < 60; index++) {
+  for (let index = 0; index < 200; index++) {
     result.push({
       id: `${index}`,
       beginTime: '@datetime',

+ 8 - 0
mock/sys/user.ts

@@ -111,4 +111,12 @@ export default [
       return resultSuccess(undefined, { message: 'Token has been destroyed' });
     },
   },
+  {
+    url: '/basic-api/testRetry',
+    statusCode: 405,
+    method: 'get',
+    response: () => {
+      return resultError('Error!');
+    },
+  },
 ] as MockMethod[];

+ 38 - 29
package.json

@@ -1,6 +1,6 @@
 {
   "name": "vben-admin",
-  "version": "2.7.2",
+  "version": "2.8.0",
   "author": {
     "name": "vben",
     "email": "anncwb@126.com",
@@ -48,12 +48,12 @@
     }
   },
   "scripts": {
-    "bootstrap": "yarn install",
-    "dev": "vite",
+    "bootstrap": "pnpm install",
     "serve": "npm run dev",
+    "dev": "vite",
     "build": "cross-env NODE_ENV=production vite build && esno ./build/script/postBuild.ts",
     "build:test": "cross-env vite build --mode test && esno ./build/script/postBuild.ts",
-    "build:no-cache": "yarn clean:cache && npm run build",
+    "build:no-cache": "pnpm clean:cache && npm run build",
     "dev:app": "esno ./build/script/startElectron.ts --env=development --watch",
     "build:app": "npm run build && esno ./build/script/startElectron.ts --env=production && electron-builder ",
     "report": "cross-env REPORT=true npm run build",
@@ -66,48 +66,53 @@
     "lint:eslint": "eslint --cache --max-warnings 0  \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
     "lint:prettier": "prettier --write  \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
     "lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
-    "lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
-    "lint:pretty": "pretty-quick --staged",
+    "lint:lint-staged": "lint-staged",
     "test:unit": "jest",
-    "test:unit-coverage": "jest --coverage",
-    "test:gzip": "http-server dist --cors --gzip -c-1",
-    "test:br": "http-server dist --cors --brotli -c-1",
-    "reinstall": "rimraf yarn.lock && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
+    "test:gzip": "npx http-server dist --cors --gzip -c-1",
+    "test:br": "npx http-server dist --cors --brotli -c-1",
+    "reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
     "prepare": "husky install",
     "gen:icon": "esno ./build/generate/icon/index.ts"
   },
   "dependencies": {
-    "@iconify/iconify": "^2.0.4",
+    "@ant-design/colors": "^6.0.0",
+    "@ant-design/icons-vue": "^6.1.0",
     "@logicflow/core": "^0.6.16",
     "@logicflow/extension": "^0.6.16",
+    "@iconify/iconify": "^2.2.1",
+    "@vue/runtime-core": "^3.2.31",
+    "@vue/shared": "^3.2.31",
     "@vueuse/core": "^6.3.3",
-    "@zxcvbn-ts/core": "^1.0.0-beta.0",
-    "ant-design-vue": "2.2.7",
+    "@vueuse/shared": "^6.3.3",
+    "@zxcvbn-ts/core": "^2.0.1",
+    "ant-design-vue": "3.1.1",
     "axios": "^0.21.4",
     "codemirror": "^5.62.3",
     "cropperjs": "^1.5.12",
     "electron-is-dev": "^1.2.0",
     "crypto-js": "^4.1.1",
+    "dayjs": "^1.11.0",
     "echarts": "^5.2.0",
     "intro.js": "^4.2.2",
     "lodash-es": "^4.17.21",
     "mockjs": "^1.1.0",
     "nprogress": "^0.2.0",
     "path-to-regexp": "^6.2.0",
-    "pinia": "2.0.0-rc.9",
+    "pinia": "2.0.12",
     "print-js": "^1.6.0",
     "qrcode": "^1.4.4",
+    "qs": "^6.10.3",
     "resize-observer-polyfill": "^1.5.1",
     "showdown": "^1.9.1",
     "sortablejs": "^1.14.0",
     "tinymce": "^5.9.2",
-    "vditor": "^3.8.6",
-    "vue": "3.2.11",
-    "vue-i18n": "9.1.7",
-    "vue-json-pretty": "^2.0.4",
-    "vue-router": "^4.0.11",
-    "vue-types": "^4.1.0",
-    "xlsx": "^0.17.1"
+    "vditor": "^3.8.13",
+    "vue": "^3.2.31",
+    "vue-i18n": "^9.1.9",
+    "vue-json-pretty": "^2.0.6",
+    "vue-router": "^4.0.14",
+    "vue-types": "^4.1.1",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@commitlint/cli": "^13.1.0",
@@ -144,7 +149,7 @@
     "conventional-changelog-cli": "^2.1.1",
     "cross-env": "^7.0.3",
     "dotenv": "^10.0.0",
-    "electron": "^11.0.0",
+    "electron": "^18.0.0",
     "electron-builder": "^22.8.0",
     "electron-connect": "^0.6.3",
     "electron-contextmenu-middleware": "^1.0.3",
@@ -163,9 +168,12 @@
     "is-ci": "^3.0.0",
     "jest": "^27.2.0",
     "less": "^4.1.1",
-    "lint-staged": "^11.1.2",
+    "lint-staged": "12.3.7",
     "npm-run-all": "^4.1.5",
+    "picocolors": "^1.0.0",
     "postcss": "^8.3.6",
+    "postcss-html": "^1.3.0",
+    "postcss-less": "^6.0.0",
     "prettier": "^2.4.0",
     "pretty-quick": "^3.1.1",
     "rimraf": "^3.0.2",
@@ -178,15 +186,16 @@
     "ts-jest": "^27.0.5",
     "ts-node": "^10.2.1",
     "typescript": "4.4.3",
-    "vite": "2.5.7",
+    "vite": "^2.9.1",
     "vite-plugin-compression": "^0.3.5",
-    "vite-plugin-html": "^2.1.0",
+    "vite-plugin-html": "^3.2.0",
     "vite-plugin-imagemin": "^0.4.5",
+    "vite-plugin-mkcert": "^1.6.0",
     "vite-plugin-mock": "^2.9.6",
     "vite-plugin-purge-icons": "^0.7.0",
     "vite-plugin-pwa": "^0.11.2",
-    "vite-plugin-style-import": "^1.2.1",
-    "vite-plugin-svg-icons": "^1.0.4",
+    "vite-plugin-style-import": "^2.0.0",
+    "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-theme": "^0.8.1",
     "wait-on": "^5.2.1",
     "vite-plugin-vue-setup-extend": "^0.1.0",
@@ -195,9 +204,9 @@
     "vue-tsc": "^0.3.0"
   },
   "resolutions": {
-    "//": "Used to install imagemin dependencies, because imagemin may not be installed in China. If it is abroad, you can delete it",
     "bin-wrapper": "npm:bin-wrapper-china",
-    "rollup": "^2.56.3"
+    "rollup": "^2.56.3",
+    "gifsicle": "5.2.0"
   },
   "repository": {
     "type": "git",

+ 0 - 8
prettier.config.js

@@ -1,17 +1,9 @@
 module.exports = {
   printWidth: 100,
-  tabWidth: 2,
-  useTabs: false,
   semi: true,
   vueIndentScriptAndStyle: true,
   singleQuote: true,
-  quoteProps: 'as-needed',
-  bracketSpacing: true,
   trailingComma: 'all',
-  jsxSingleQuote: false,
-  arrowParens: 'always',
-  insertPragma: false,
-  requirePragma: false,
   proseWrap: 'never',
   htmlWhitespaceSensitivity: 'strict',
   endOfLine: 'auto',

+ 1 - 0
src/App.vue

@@ -12,6 +12,7 @@
   import { useTitle } from '/@/hooks/web/useTitle';
   import { useLocale } from '/@/locales/useLocale';
 
+  import 'dayjs/locale/zh-cn';
   // support Multi-language
   const { getAntdLocale } = useLocale();
 

+ 3 - 0
src/api/demo/account.ts

@@ -4,6 +4,7 @@ import { GetAccountInfoModel } from './model/accountModel';
 enum Api {
   ACCOUNT_INFO = '/account/getAccountInfo',
   SESSION_TIMEOUT = '/user/sessionTimeout',
+  TOKEN_EXPIRED = '/user/tokenExpired',
 }
 
 // Get personal center-basic settings
@@ -11,3 +12,5 @@ enum Api {
 export const accountInfoApi = () => defHttp.get<GetAccountInfoModel>({ url: Api.ACCOUNT_INFO });
 
 export const sessionTimeoutApi = () => defHttp.post<void>({ url: Api.SESSION_TIMEOUT });
+
+export const tokenExpiredApi = () => defHttp.post<void>({ url: Api.TOKEN_EXPIRED });

+ 9 - 0
src/api/demo/cascader.ts

@@ -0,0 +1,9 @@
+import { defHttp } from '/@/utils/http/axios';
+import { AreaModel, AreaParams } from '/@/api/demo/model/areaModel';
+
+enum Api {
+  AREA_RECORD = '/cascader/getAreaRecord',
+}
+
+export const areaRecord = (data: AreaParams) =>
+  defHttp.post<AreaModel>({ url: Api.AREA_RECORD, data });

+ 12 - 0
src/api/demo/model/areaModel.ts

@@ -0,0 +1,12 @@
+export interface AreaModel {
+  id: string;
+  code: string;
+  parentCode: string;
+  name: string;
+  levelType: number;
+  [key: string]: string | number;
+}
+
+export interface AreaParams {
+  parentCode: string;
+}

+ 1 - 0
src/api/demo/table.ts

@@ -14,6 +14,7 @@ export const demoListApi = (params: DemoParams) =>
     url: Api.DEMO_LIST,
     params,
     headers: {
+      // @ts-ignore
       ignoreCancelToken: true,
     },
   });

+ 1 - 1
src/api/model/baseModel.ts

@@ -3,7 +3,7 @@ export interface BasicPageParams {
   pageSize: number;
 }
 
-export interface BasicFetchResult<T extends any> {
+export interface BasicFetchResult<T> {
   items: T[];
   total: number;
 }

+ 14 - 0
src/api/sys/user.ts

@@ -8,6 +8,7 @@ enum Api {
   Logout = '/logout',
   GetUserInfo = '/getUserInfo',
   GetPermCode = '/getPermCode',
+  TestRetry = '/testRetry',
 }
 
 /**
@@ -39,3 +40,16 @@ export function getPermCode() {
 export function doLogout() {
   return defHttp.get({ url: Api.Logout });
 }
+
+export function testRetry() {
+  return defHttp.get(
+    { url: Api.TestRetry },
+    {
+      retryRequest: {
+        isOpenRetry: true,
+        count: 5,
+        waitTime: 1000,
+      },
+    },
+  );
+}

+ 1 - 1
src/components/Application/src/AppDarkModeToggle.vue

@@ -39,7 +39,7 @@
 
   html[data-theme='dark'] {
     .@{prefix-cls} {
-      border: 1px solid rgb(196, 188, 188);
+      border: 1px solid rgb(196 188 188);
     }
   }
 

+ 2 - 2
src/components/Application/src/AppLocalePicker.vue

@@ -4,11 +4,11 @@
 -->
 <template>
   <Dropdown
-    placement="bottomCenter"
+    placement="bottom"
     :trigger="['click']"
     :dropMenuList="localeList"
     :selectedKeys="selectedKeys"
-    @menuEvent="handleMenuEvent"
+    @menu-event="handleMenuEvent"
     overlayClassName="app-locale-picker-overlay"
   >
     <span class="cursor-pointer flex items-center">

+ 1 - 0
src/components/Application/src/AppLogo.vue

@@ -87,6 +87,7 @@
       font-size: 16px;
       font-weight: 700;
       transition: all 0.5s;
+      line-height: normal;
     }
   }
 </style>

+ 1 - 1
src/components/Application/src/search/AppSearchFooter.vue

@@ -42,7 +42,7 @@
       background-color: linear-gradient(-225deg, #d5dbe4, #f8f8f8);
       border-radius: 2px;
       box-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff,
-        0 1px 2px 1px rgba(30, 35, 90, 0.4);
+        0 1px 2px 1px rgb(30 35 90 / 40%);
       align-items: center;
       justify-content: center;
 

+ 7 - 7
src/components/Application/src/search/AppSearchModal.vue

@@ -125,7 +125,7 @@
     width: 100%;
     height: 100%;
     padding-top: 50px;
-    background-color: rgba(0, 0, 0, 0.25);
+    background-color: rgb(0 0 0 / 25%);
     justify-content: center;
 
     &--mobile {
@@ -159,7 +159,7 @@
 
         &__item {
           &-enter {
-            opacity: 0 !important;
+            opacity: 0% !important;
           }
         }
       }
@@ -168,16 +168,16 @@
     &-content {
       position: relative;
       width: 632px;
-      margin: 0 auto auto auto;
+      margin: 0 auto auto;
       background-color: @component-background;
       border-radius: 16px;
-      box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+      box-shadow: 0 25px 50px -12px rgb(0 0 0 / 25%);
       flex-direction: column;
     }
 
     &-input__wrapper {
       display: flex;
-      padding: 14px 14px 0 14px;
+      padding: 14px 14px 0;
       justify-content: space-between;
       align-items: center;
     }
@@ -245,7 +245,7 @@
           background-color: @primary-color;
 
           .@{prefix-cls}-list__item-enter {
-            opacity: 1;
+            opacity: 100%;
           }
         }
 
@@ -259,7 +259,7 @@
 
         &-enter {
           width: 30px;
-          opacity: 0;
+          opacity: 0%;
         }
       }
     }

+ 4 - 5
src/components/CardList/src/CardList.vue

@@ -1,10 +1,9 @@
 <template>
   <div class="p-2">
-    <div class="bg-white mb-2 p-4">
+    <div class="p-4 mb-2 bg-white">
       <BasicForm @register="registerForm" />
     </div>
-    {{ sliderProp.width }}
-    <div class="bg-white p-2">
+    <div class="p-2 bg-white">
       <List
         :grid="{ gutter: 5, xs: 1, sm: 2, md: 4, lg: 4, xl: 6, xxl: grid }"
         :data-source="data"
@@ -39,7 +38,7 @@
                   <Image :src="item.imgs[0]" />
                 </div>
               </template>
-              <template class="ant-card-actions" #actions>
+              <template #actions>
                 <!--              <SettingOutlined key="setting" />-->
                 <EditOutlined key="edit" />
                 <Dropdown
@@ -167,7 +166,7 @@
     pageSize.value = pz;
     fetch();
   }
-  function pageSizeChange(current, size) {
+  function pageSizeChange(_current, size) {
     pageSize.value = size;
     fetch();
   }

+ 1 - 1
src/components/CardList/src/data.ts

@@ -1,5 +1,5 @@
 import { ref } from 'vue';
-//每行个数
+// 每行个数
 export const grid = ref(12);
 // slider属性
 export const useSlider = (min = 6, max = 12) => {

+ 2 - 0
src/components/CodeEditor/index.ts

@@ -4,3 +4,5 @@ import jsonPreview from './src/json-preview/JsonPreview.vue';
 
 export const CodeEditor = withInstall(codeEditor);
 export const JsonPreview = withInstall(jsonPreview);
+
+export * from './src/typing';

+ 9 - 9
src/components/CodeEditor/src/CodeEditor.vue

@@ -8,22 +8,22 @@
     />
   </div>
 </template>
-
-<script lang="ts">
-  export const MODE = {
-    JSON: 'application/json',
-    html: 'htmlmixed',
-    js: 'javascript',
-  };
-</script>
 <script lang="ts" setup>
   import { computed } from 'vue';
   import CodeMirrorEditor from './codemirror/CodeMirror.vue';
   import { isString } from '/@/utils/is';
+  import { MODE } from './typing';
 
   const props = defineProps({
     value: { type: [Object, String] as PropType<Record<string, any> | string> },
-    mode: { type: String, default: MODE.JSON },
+    mode: {
+      type: String as PropType<MODE>,
+      default: MODE.JSON,
+      validator(value: any) {
+        // 这个值必须匹配下列字符串中的一个
+        return Object.values(MODE).includes(value);
+      },
+    },
     readonly: { type: Boolean },
     autoFormat: { type: Boolean, default: true },
   });

+ 9 - 1
src/components/CodeEditor/src/codemirror/CodeMirror.vue

@@ -8,6 +8,7 @@
   import { useAppStore } from '/@/store/modules/app';
   import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
   import CodeMirror from 'codemirror';
+  import { MODE } from './../typing';
   // css
   import './codemirror.css';
   import 'codemirror/theme/idea.css';
@@ -18,7 +19,14 @@
   import 'codemirror/mode/htmlmixed/htmlmixed';
 
   const props = defineProps({
-    mode: { type: String, default: 'application/json' },
+    mode: {
+      type: String as PropType<MODE>,
+      default: MODE.JSON,
+      validator(value: any) {
+        // 这个值必须匹配下列字符串中的一个
+        return Object.values(MODE).includes(value);
+      },
+    },
     value: { type: String, default: '' },
     readonly: { type: Boolean, default: false },
   });

+ 7 - 21
src/components/CodeEditor/src/codemirror/codemirror.css

@@ -2,7 +2,7 @@
 
 .CodeMirror {
   --base: #545281;
-  --comment: hsl(210, 25%, 60%);
+  --comment: hsl(210deg 25% 60%);
   --keyword: #af4ab1;
   --variable: #0055d1;
   --function: #c25205;
@@ -125,9 +125,7 @@
 }
 
 .cm-fat-cursor-mark {
-  background-color: rgba(20, 255, 20, 0.5);
-  -webkit-animation: blink 1.06s steps(1) infinite;
-  -moz-animation: blink 1.06s steps(1) infinite;
+  background-color: rgb(20 255 20 / 50%);
   animation: blink 1.06s steps(1) infinite;
 }
 
@@ -135,16 +133,14 @@
   width: auto;
   background-color: #7e7;
   border: 0;
-  -webkit-animation: blink 1.06s steps(1) infinite;
-  -moz-animation: blink 1.06s steps(1) infinite;
   animation: blink 1.06s steps(1) infinite;
 }
-@-moz-keyframes blink {
+@keyframes blink {
   50% {
     background-color: transparent;
   }
 }
-@-webkit-keyframes blink {
+@keyframes blink {
   50% {
     background-color: transparent;
   }
@@ -294,7 +290,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {
 }
 
 .CodeMirror-matchingtag {
-  background: rgba(255, 150, 0, 0.3);
+  background: rgb(255 150 0 / 30%);
 }
 
 .CodeMirror-activeline-background {
@@ -394,7 +390,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {
   background-color: transparent;
 }
 
-.CodeMirror-gutter-wrapper ::-moz-selection {
+.CodeMirrorwrapper ::selection {
   background-color: transparent;
 }
 
@@ -414,11 +410,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {
   border-width: 0;
 
   /* Reset some styles that the rest of the page might have set */
-  -moz-border-radius: 0;
-  -webkit-border-radius: 0;
   border-radius: 0;
   -webkit-tap-highlight-color: transparent;
-  -webkit-font-variant-ligatures: contextual;
   font-variant-ligatures: contextual;
 }
 
@@ -457,7 +450,6 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {
 .CodeMirror-gutter,
 .CodeMirror-gutters,
 .CodeMirror-linenumber {
-  -moz-box-sizing: content-box;
   box-sizing: content-box;
 }
 
@@ -505,15 +497,9 @@ div.CodeMirror-dragcursors {
   background: #d7d4f0;
 }
 
-.CodeMirror-line::-moz-selection,
-.CodeMirror-line > span::-moz-selection,
-.CodeMirror-line > span > span::-moz-selection {
-  background: #d7d4f0;
-}
-
 .cm-searching {
   background-color: #ffa;
-  background-color: rgba(255, 255, 0, 0.4);
+  background-color: rgb(255 255 0 / 40%);
 }
 
 /* Used to force a border model for a node */

+ 5 - 0
src/components/CodeEditor/src/typing.ts

@@ -0,0 +1,5 @@
+export enum MODE {
+  JSON = 'application/json',
+  HTML = 'htmlmixed',
+  JS = 'javascript',
+}

+ 8 - 3
src/components/Container/src/collapse/CollapseContainer.vue

@@ -1,6 +1,6 @@
 <template>
   <div :class="prefixCls">
-    <CollapseHeader v-bind="$props" :prefixCls="prefixCls" :show="show" @expand="handleExpand">
+    <CollapseHeader v-bind="props" :prefixCls="prefixCls" :show="show" @expand="handleExpand">
       <template #title>
         <slot name="title"></slot>
       </template>
@@ -25,6 +25,7 @@
 <script lang="ts" setup>
   import type { PropType } from 'vue';
   import { ref } from 'vue';
+  import { isNil } from 'lodash-es';
   // component
   import { Skeleton } from 'ant-design-vue';
   import { CollapseTransition } from '/@/components/Transition';
@@ -66,13 +67,17 @@
   /**
    * @description: Handling development events
    */
-  function handleExpand() {
-    show.value = !show.value;
+  function handleExpand(val: boolean) {
+    show.value = isNil(val) ? !show.value : val;
     if (props.triggerWindowResize) {
       // 200 milliseconds here is because the expansion has animation,
       useTimeoutFn(triggerWindowResize, 200);
     }
   }
+
+  defineExpose({
+    handleExpand,
+  });
 </script>
 <style lang="less">
   @prefix-cls: ~'@{namespace}-collapse-container';

+ 15 - 14
src/components/ContextMenu/src/ContextMenu.vue

@@ -60,9 +60,11 @@
         const top = body.clientHeight < y + menuHeight ? y - menuHeight : y;
         return {
           ...styles,
+          position: 'absolute',
           width: `${width}px`,
           left: `${left + 1}px`,
           top: `${top + 1}px`,
+          zIndex: 9999,
         };
       });
 
@@ -124,15 +126,11 @@
         }
         const { items } = props;
         return (
-          <Menu
-            inlineIndent={12}
-            mode="vertical"
-            class={prefixCls}
-            ref={wrapRef}
-            style={unref(getStyle)}
-          >
-            {renderMenuItem(items)}
-          </Menu>
+          <div class={prefixCls}>
+            <Menu inlineIndent={12} mode="vertical" ref={wrapRef} style={unref(getStyle)}>
+              {renderMenuItem(items)}
+            </Menu>
+          </div>
         );
       };
     },
@@ -178,22 +176,25 @@
     margin: 0;
     list-style: none;
     background-color: @component-background;
-    border: 1px solid rgba(0, 0, 0, 0.08);
+    border: 1px solid rgb(0 0 0 / 8%);
     border-radius: 0.25rem;
-    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.1),
-      0 1px 5px 0 rgba(0, 0, 0, 0.06);
+    box-shadow: 0 2px 2px 0 rgb(0 0 0 / 14%), 0 3px 1px -2px rgb(0 0 0 / 10%),
+      0 1px 5px 0 rgb(0 0 0 / 6%);
     background-clip: padding-box;
     user-select: none;
 
+    &__item {
+      margin: 0 !important;
+    }
     .item-style();
 
     .ant-divider {
-      margin: 0 0;
+      margin: 0;
     }
 
     &__popup {
       .ant-divider {
-        margin: 0 0;
+        margin: 0;
       }
 
       .item-style();

+ 4 - 4
src/components/Cropper/src/CopperModal.vue

@@ -234,17 +234,17 @@
       background: #eee;
       background-image: linear-gradient(
           45deg,
-          rgba(0, 0, 0, 0.25) 25%,
+          rgb(0 0 0 / 25%) 25%,
           transparent 0,
           transparent 75%,
-          rgba(0, 0, 0, 0.25) 0
+          rgb(0 0 0 / 25%) 0
         ),
         linear-gradient(
           45deg,
-          rgba(0, 0, 0, 0.25) 25%,
+          rgb(0 0 0 / 25%) 25%,
           transparent 0,
           transparent 75%,
-          rgba(0, 0, 0, 0.25) 0
+          rgb(0 0 0 / 25%) 0
         );
       background-position: 0 0, 12px 12px;
       background-size: 24px 24px;

+ 4 - 5
src/components/Cropper/src/CropperAvatar.vue

@@ -22,7 +22,7 @@
 
     <CopperModal
       @register="register"
-      @uploadSuccess="handleUploadSuccess"
+      @upload-success="handleUploadSuccess"
       :uploadApi="uploadApi"
       :src="sourceValue"
     />
@@ -135,15 +135,14 @@
     }
 
     &-image-mask {
-      opacity: 0;
+      opacity: 0%;
       position: absolute;
       width: inherit;
       height: inherit;
       border-radius: inherit;
       border: inherit;
-      background: rgba(0, 0, 0, 0.4);
+      background: rgb(0 0 0 / 40%);
       cursor: pointer;
-      -webkit-transition: opacity 0.4s;
       transition: opacity 0.4s;
 
       ::v-deep(svg) {
@@ -152,7 +151,7 @@
     }
 
     &-image-mask:hover {
-      opacity: 40;
+      opacity: 4000%;
     }
 
     &-upload-btn {

+ 4 - 1
src/components/Description/src/Description.vue

@@ -3,7 +3,7 @@
   import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index';
   import type { CSSProperties } from 'vue';
   import type { CollapseContainerOptions } from '/@/components/Container/index';
-  import { defineComponent, computed, ref, unref } from 'vue';
+  import { defineComponent, computed, ref, unref, toRefs } from 'vue';
   import { get } from 'lodash-es';
   import { Descriptions } from 'ant-design-vue';
   import { CollapseContainer } from '/@/components/Container/index';
@@ -121,6 +121,9 @@
                 return null;
               }
               const getField = get(_data, field);
+              if (getField && !toRefs(_data).hasOwnProperty(field)) {
+                return isFunction(render) ? render('', _data) : '';
+              }
               return isFunction(render) ? render(getField, _data) : getField ?? '';
             };
 

+ 1 - 1
src/components/Drawer/src/BasicDrawer.vue

@@ -94,7 +94,7 @@
             opt.width = '100%';
           }
           const detailCls = `${prefixCls}__detail`;
-          opt.wrapClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
+          opt.class = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
 
           if (!getContainer) {
             // TODO type error?

+ 1 - 2
src/components/Drawer/src/typing.ts

@@ -128,13 +128,12 @@ export interface DrawerProps extends DrawerFooterProps {
    * @type any (string | slot)
    */
   title?: VNodeChild | JSX.Element;
-
   /**
    * The class name of the container of the Drawer dialog.
    * @type string
    */
   wrapClassName?: string;
-
+  class?: string;
   /**
    * Style of wrapper element which **contains mask** compare to `drawerStyle`
    * @type object

+ 55 - 66
src/components/Dropdown/src/Dropdown.vue

@@ -1,17 +1,17 @@
 <template>
-  <Dropdown :trigger="trigger" v-bind="$attrs">
+  <a-dropdown :trigger="trigger" v-bind="$attrs">
     <span>
       <slot></slot>
     </span>
     <template #overlay>
-      <Menu :selectedKeys="selectedKeys">
+      <a-menu :selectedKeys="selectedKeys">
         <template v-for="item in dropMenuList" :key="`${item.event}`">
-          <MenuItem
+          <a-menu-item
             v-bind="getAttr(item.event)"
             @click="handleClickMenu(item)"
             :disabled="item.disabled"
           >
-            <Popconfirm
+            <a-popconfirm
               v-if="popconfirm && item.popConfirm"
               v-bind="getPopConfirmAttrs(item.popConfirm)"
             >
@@ -22,86 +22,75 @@
                 <Icon :icon="item.icon" v-if="item.icon" />
                 <span class="ml-1">{{ item.text }}</span>
               </div>
-            </Popconfirm>
+            </a-popconfirm>
             <template v-else>
               <Icon :icon="item.icon" v-if="item.icon" />
               <span class="ml-1">{{ item.text }}</span>
             </template>
-          </MenuItem>
-          <MenuDivider v-if="item.divider" :key="`d-${item.event}`" />
+          </a-menu-item>
+          <a-menu-divider v-if="item.divider" :key="`d-${item.event}`" />
         </template>
-      </Menu>
+      </a-menu>
     </template>
-  </Dropdown>
+  </a-dropdown>
 </template>
 
-<script lang="ts">
+<script lang="ts" setup>
   import { computed, PropType } from 'vue';
   import type { DropMenu } from './typing';
-
-  import { defineComponent } from 'vue';
   import { Dropdown, Menu, Popconfirm } from 'ant-design-vue';
   import { Icon } from '/@/components/Icon';
   import { omit } from 'lodash-es';
   import { isFunction } from '/@/utils/is';
 
-  export default defineComponent({
-    name: 'BasicDropdown',
-    components: {
-      Dropdown,
-      Menu,
-      MenuItem: Menu.Item,
-      MenuDivider: Menu.Divider,
-      Icon,
-      Popconfirm,
-    },
-    props: {
-      popconfirm: Boolean,
-      /**
-       * the trigger mode which executes the drop-down action
-       * @default ['hover']
-       * @type string[]
-       */
-      trigger: {
-        type: [Array] as PropType<('contextmenu' | 'click' | 'hover')[]>,
-        default: () => {
-          return ['contextmenu'];
-        },
-      },
-      dropMenuList: {
-        type: Array as PropType<(DropMenu & Recordable)[]>,
-        default: () => [],
-      },
-      selectedKeys: {
-        type: Array as PropType<string[]>,
-        default: () => [],
+  const ADropdown = Dropdown;
+  const AMenu = Menu;
+  const AMenuItem = Menu.Item;
+  const AMenuDivider = Menu.Divider;
+  const APopconfirm = Popconfirm;
+
+  const props = defineProps({
+    popconfirm: Boolean,
+    /**
+     * the trigger mode which executes the drop-down action
+     * @default ['hover']
+     * @type string[]
+     */
+    trigger: {
+      type: [Array] as PropType<('contextmenu' | 'click' | 'hover')[]>,
+      default: () => {
+        return ['contextmenu'];
       },
     },
-    emits: ['menuEvent'],
-    setup(props, { emit }) {
-      function handleClickMenu(item: DropMenu) {
-        const { event } = item;
-        const menu = props.dropMenuList.find((item) => `${item.event}` === `${event}`);
-        emit('menuEvent', menu);
-        item.onClick?.();
-      }
+    dropMenuList: {
+      type: Array as PropType<(DropMenu & Recordable)[]>,
+      default: () => [],
+    },
+    selectedKeys: {
+      type: Array as PropType<string[]>,
+      default: () => [],
+    },
+  });
 
-      const getPopConfirmAttrs = computed(() => {
-        return (attrs) => {
-          const originAttrs = omit(attrs, ['confirm', 'cancel', 'icon']);
-          if (!attrs.onConfirm && attrs.confirm && isFunction(attrs.confirm))
-            originAttrs['onConfirm'] = attrs.confirm;
-          if (!attrs.onCancel && attrs.cancel && isFunction(attrs.cancel))
-            originAttrs['onCancel'] = attrs.cancel;
-          return originAttrs;
-        };
-      });
+  const emit = defineEmits(['menuEvent']);
 
-      return {
-        handleClickMenu,
-        getPopConfirmAttrs,
-        getAttr: (key: string | number) => ({ key }),
-      };
-    },
+  function handleClickMenu(item: DropMenu) {
+    const { event } = item;
+    const menu = props.dropMenuList.find((item) => `${item.event}` === `${event}`);
+    emit('menuEvent', menu);
+    item.onClick?.();
+  }
+
+  const getPopConfirmAttrs = computed(() => {
+    return (attrs) => {
+      const originAttrs = omit(attrs, ['confirm', 'cancel', 'icon']);
+      if (!attrs.onConfirm && attrs.confirm && isFunction(attrs.confirm))
+        originAttrs['onConfirm'] = attrs.confirm;
+      if (!attrs.onCancel && attrs.cancel && isFunction(attrs.cancel))
+        originAttrs['onCancel'] = attrs.cancel;
+      return originAttrs;
+    };
   });
+
+  const getAttr = (key: string | number) => ({ key });
 </script>

+ 1 - 1
src/components/Excel/src/Export2Excel.ts

@@ -1,4 +1,4 @@
-import xlsx from 'xlsx';
+import * as xlsx from 'xlsx';
 import type { WorkBook } from 'xlsx';
 import type { JsonToSheet, AoAToSheet } from './typing';
 

+ 35 - 4
src/components/Excel/src/ImportExcel.vue

@@ -14,13 +14,26 @@
 </template>
 <script lang="ts">
   import { defineComponent, ref, unref } from 'vue';
-  import XLSX from 'xlsx';
+  import * as XLSX from 'xlsx';
+  import { dateUtil } from '/@/utils/dateUtil';
 
   import type { ExcelData } from './typing';
   export default defineComponent({
     name: 'ImportExcel',
+    props: {
+      // 日期时间格式。如果不提供或者提供空值,将返回原始Date对象
+      dateFormat: {
+        type: String,
+      },
+      // 时区调整。实验性功能,仅为了解决读取日期时间值有偏差的问题。目前仅提供了+08:00时区的偏差修正值
+      // https://github.com/SheetJS/sheetjs/issues/1470#issuecomment-501108554
+      timeZone: {
+        type: Number,
+        default: 8,
+      },
+    },
     emits: ['success', 'error'],
-    setup(_, { emit }) {
+    setup(props, { emit }) {
       const inputRef = ref<HTMLInputElement | null>(null);
       const loadingRef = ref<Boolean>(false);
 
@@ -51,10 +64,28 @@
        */
       function getExcelData(workbook: XLSX.WorkBook) {
         const excelData: ExcelData[] = [];
+        const { dateFormat, timeZone } = props;
         for (const sheetName of workbook.SheetNames) {
           const worksheet = workbook.Sheets[sheetName];
           const header: string[] = getHeaderRow(worksheet);
-          const results = XLSX.utils.sheet_to_json(worksheet);
+          let results = XLSX.utils.sheet_to_json(worksheet, {
+            raw: true,
+            dateNF: dateFormat, //Not worked
+          }) as object[];
+          results = results.map((row: object) => {
+            for (let field in row) {
+              if (row[field] instanceof Date) {
+                if (timeZone === 8) {
+                  row[field].setSeconds(row[field].getSeconds() + 43);
+                }
+                if (dateFormat) {
+                  row[field] = dateUtil(row[field]).format(dateFormat);
+                }
+              }
+            }
+            return row;
+          });
+
           excelData.push({
             header,
             results,
@@ -76,7 +107,7 @@
           reader.onload = async (e) => {
             try {
               const data = e.target && e.target.result;
-              const workbook = XLSX.read(data, { type: 'array' });
+              const workbook = XLSX.read(data, { type: 'array', cellDates: true });
               // console.log(workbook);
               /* DO SOMETHING WITH workbook HERE */
               const excelData = getExcelData(workbook);

+ 3 - 0
src/components/Form/index.ts

@@ -9,5 +9,8 @@ export { useForm } from './src/hooks/useForm';
 export { default as ApiSelect } from './src/components/ApiSelect.vue';
 export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue';
 export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue';
+export { default as ApiTree } from './src/components/ApiTree.vue';
+export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
+export { default as ApiCascader } from './src/components/ApiCascader.vue';
 
 export { BasicForm };

+ 12 - 2
src/components/Form/src/BasicForm.vue

@@ -58,6 +58,7 @@
   import { createFormContext } from './hooks/useFormContext';
   import { useAutoFocus } from './hooks/useAutoFocus';
   import { useModalContext } from '/@/components/Modal';
+  import { useDebounceFn } from '@vueuse/core';
 
   import { basicProps } from './props';
   import { useDesign } from '/@/hooks/web/useDesign';
@@ -66,7 +67,7 @@
     name: 'BasicForm',
     components: { FormItem, Form, Row, FormAction },
     props: basicProps,
-    emits: ['advanced-change', 'reset', 'submit', 'register'],
+    emits: ['advanced-change', 'reset', 'submit', 'register', 'field-value-change'],
     setup(props, { emit, attrs }) {
       const formModel = reactive<Recordable>({});
       const modalFn = useModalContext();
@@ -122,7 +123,7 @@
             if (!Array.isArray(defaultValue)) {
               schema.defaultValue = dateUtil(defaultValue);
             } else {
-              const def: moment.Moment[] = [];
+              const def: any[] = [];
               defaultValue.forEach((item) => {
                 def.push(dateUtil(item));
               });
@@ -225,6 +226,14 @@
         },
       );
 
+      watch(
+        () => formModel,
+        useDebounceFn(() => {
+          unref(getProps).submitOnChange && handleSubmit();
+        }, 300),
+        { deep: true },
+      );
+
       async function setProps(formProps: Partial<FormProps>): Promise<void> {
         propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
       }
@@ -235,6 +244,7 @@
         if (!validateTrigger || validateTrigger === 'change') {
           validateFields([key]).catch((_) => {});
         }
+        emit('field-value-change', key, value);
       }
 
       function handleEnterPress(e: KeyboardEvent) {

+ 6 - 0
src/components/Form/src/componentMap.ts

@@ -21,9 +21,12 @@ import {
   Divider,
 } from 'ant-design-vue';
 
+import ApiRadioGroup from './components/ApiRadioGroup.vue';
 import RadioButtonGroup from './components/RadioButtonGroup.vue';
 import ApiSelect from './components/ApiSelect.vue';
+import ApiTree from './components/ApiTree.vue';
 import ApiTreeSelect from './components/ApiTreeSelect.vue';
+import ApiCascader from './components/ApiCascader.vue';
 import { BasicUpload } from '/@/components/Upload';
 import { StrengthMeter } from '/@/components/StrengthMeter';
 import { IconPicker } from '/@/components/Icon';
@@ -41,13 +44,16 @@ componentMap.set('AutoComplete', AutoComplete);
 
 componentMap.set('Select', Select);
 componentMap.set('ApiSelect', ApiSelect);
+componentMap.set('ApiTree', ApiTree);
 componentMap.set('TreeSelect', TreeSelect);
 componentMap.set('ApiTreeSelect', ApiTreeSelect);
+componentMap.set('ApiRadioGroup', ApiRadioGroup);
 componentMap.set('Switch', Switch);
 componentMap.set('RadioButtonGroup', RadioButtonGroup);
 componentMap.set('RadioGroup', Radio.Group);
 componentMap.set('Checkbox', Checkbox);
 componentMap.set('CheckboxGroup', Checkbox.Group);
+componentMap.set('ApiCascader', ApiCascader);
 componentMap.set('Cascader', Cascader);
 componentMap.set('Slider', Slider);
 componentMap.set('Rate', Rate);

+ 198 - 0
src/components/Form/src/components/ApiCascader.vue

@@ -0,0 +1,198 @@
+<template>
+  <a-cascader
+    v-model:value="state"
+    :options="options"
+    :load-data="loadData"
+    change-on-select
+    @change="handleChange"
+    :displayRender="handleRenderDisplay"
+  >
+    <template #suffixIcon v-if="loading">
+      <LoadingOutlined spin />
+    </template>
+    <template #notFoundContent v-if="loading">
+      <span>
+        <LoadingOutlined spin class="mr-1" />
+        {{ t('component.form.apiSelectNotFound') }}
+      </span>
+    </template>
+  </a-cascader>
+</template>
+<script lang="ts">
+  import { defineComponent, PropType, ref, unref, watch, watchEffect } from 'vue';
+  import { Cascader } from 'ant-design-vue';
+  import { propTypes } from '/@/utils/propTypes';
+  import { isFunction } from '/@/utils/is';
+  import { get, omit } from 'lodash-es';
+  import { useRuleFormItem } from '/@/hooks/component/useFormItem';
+  import { LoadingOutlined } from '@ant-design/icons-vue';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  interface Option {
+    value: string;
+    label: string;
+    loading?: boolean;
+    isLeaf?: boolean;
+    children?: Option[];
+  }
+  export default defineComponent({
+    name: 'ApiCascader',
+    components: {
+      LoadingOutlined,
+      [Cascader.name]: Cascader,
+    },
+    props: {
+      value: {
+        type: Array,
+      },
+      api: {
+        type: Function as PropType<(arg?: Recordable) => Promise<Option[]>>,
+        default: null,
+      },
+      numberToString: propTypes.bool,
+      resultField: propTypes.string.def(''),
+      labelField: propTypes.string.def('label'),
+      valueField: propTypes.string.def('value'),
+      childrenField: propTypes.string.def('children'),
+      asyncFetchParamKey: propTypes.string.def('parentCode'),
+      immediate: propTypes.bool.def(true),
+      // init fetch params
+      initFetchParams: {
+        type: Object as PropType<Recordable>,
+        default: () => ({}),
+      },
+      // 是否有下级,默认是
+      isLeaf: {
+        type: Function as PropType<(arg: Recordable) => boolean>,
+        default: null,
+      },
+      displayRenderArray: {
+        type: Array,
+      },
+    },
+    emits: ['change', 'defaultChange'],
+    setup(props, { emit }) {
+      const apiData = ref<any[]>([]);
+      const options = ref<Option[]>([]);
+      const loading = ref<boolean>(false);
+      const emitData = ref<any[]>([]);
+      const isFirstLoad = ref(true);
+      const { t } = useI18n();
+      // Embedded in the form, just use the hook binding to perform form verification
+      const [state] = useRuleFormItem(props, 'value', 'change', emitData);
+
+      watch(
+        apiData,
+        (data) => {
+          const opts = generatorOptions(data);
+          options.value = opts;
+        },
+        { deep: true },
+      );
+
+      function generatorOptions(options: any[]): Option[] {
+        const { labelField, valueField, numberToString, childrenField, isLeaf } = props;
+        return options.reduce((prev, next: Recordable) => {
+          if (next) {
+            const value = next[valueField];
+            const item = {
+              ...omit(next, [labelField, valueField]),
+              label: next[labelField],
+              value: numberToString ? `${value}` : value,
+              isLeaf: isLeaf && typeof isLeaf === 'function' ? isLeaf(next) : false,
+            };
+            const children = Reflect.get(next, childrenField);
+            if (children) {
+              Reflect.set(item, childrenField, generatorOptions(children));
+            }
+            prev.push(item);
+          }
+          return prev;
+        }, [] as Option[]);
+      }
+
+      async function initialFetch() {
+        const api = props.api;
+        if (!api || !isFunction(api)) return;
+        apiData.value = [];
+        loading.value = true;
+        try {
+          const res = await api(props.initFetchParams);
+          if (Array.isArray(res)) {
+            apiData.value = res;
+            return;
+          }
+          if (props.resultField) {
+            apiData.value = get(res, props.resultField) || [];
+          }
+        } catch (error) {
+          console.warn(error);
+        } finally {
+          loading.value = false;
+        }
+      }
+
+      async function loadData(selectedOptions: Option[]) {
+        const targetOption = selectedOptions[selectedOptions.length - 1];
+        targetOption.loading = true;
+
+        const api = props.api;
+        if (!api || !isFunction(api)) return;
+        try {
+          const res = await api({
+            [props.asyncFetchParamKey]: Reflect.get(targetOption, 'value'),
+          });
+          if (Array.isArray(res)) {
+            const children = generatorOptions(res);
+            targetOption.children = children;
+            return;
+          }
+          if (props.resultField) {
+            const children = generatorOptions(get(res, props.resultField) || []);
+            targetOption.children = children;
+          }
+        } catch (e) {
+          console.error(e);
+        } finally {
+          targetOption.loading = false;
+        }
+      }
+
+      watchEffect(() => {
+        props.immediate && initialFetch();
+      });
+
+      watch(
+        () => props.initFetchParams,
+        () => {
+          !unref(isFirstLoad) && initialFetch();
+        },
+        { deep: true },
+      );
+
+      function handleChange(keys, args) {
+        emitData.value = keys;
+        emit('defaultChange', keys, args);
+      }
+
+      function handleRenderDisplay({ labels, selectedOptions }) {
+        if (unref(emitData).length === selectedOptions.length) {
+          return labels.join(' / ');
+        }
+        if (props.displayRenderArray) {
+          return props.displayRenderArray.join(' / ');
+        }
+        return '';
+      }
+
+      return {
+        state,
+        options,
+        loading,
+        t,
+        handleChange,
+        loadData,
+        handleRenderDisplay,
+      };
+    },
+  });
+</script>

+ 130 - 0
src/components/Form/src/components/ApiRadioGroup.vue

@@ -0,0 +1,130 @@
+<!--
+ * @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
+-->
+<template>
+  <RadioGroup v-bind="attrs" v-model:value="state" button-style="solid" @change="handleChange">
+    <template v-for="item in getOptions" :key="`${item.value}`">
+      <RadioButton v-if="props.isBtn" :value="item.value" :disabled="item.disabled">
+        {{ item.label }}
+      </RadioButton>
+      <Radio v-else :value="item.value" :disabled="item.disabled">
+        {{ item.label }}
+      </Radio>
+    </template>
+  </RadioGroup>
+</template>
+<script lang="ts">
+  import { defineComponent, PropType, ref, watchEffect, computed, unref, watch } from 'vue';
+  import { Radio } from 'ant-design-vue';
+  import { isFunction } from '/@/utils/is';
+  import { useRuleFormItem } from '/@/hooks/component/useFormItem';
+  import { useAttrs } from '/@/hooks/core/useAttrs';
+  import { propTypes } from '/@/utils/propTypes';
+  import { get, omit } from 'lodash-es';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
+
+  export default defineComponent({
+    name: 'ApiRadioGroup',
+    components: {
+      RadioGroup: Radio.Group,
+      RadioButton: Radio.Button,
+      Radio,
+    },
+    props: {
+      api: {
+        type: Function as PropType<(arg?: Recordable | string) => Promise<OptionsItem[]>>,
+        default: null,
+      },
+      params: {
+        type: [Object, String] as PropType<Recordable | string>,
+        default: () => ({}),
+      },
+      value: {
+        type: [String, Number, Boolean] as PropType<string | number | boolean>,
+      },
+      isBtn: {
+        type: [Boolean] as PropType<boolean>,
+        default: false,
+      },
+      numberToString: propTypes.bool,
+      resultField: propTypes.string.def(''),
+      labelField: propTypes.string.def('label'),
+      valueField: propTypes.string.def('value'),
+      immediate: propTypes.bool.def(true),
+    },
+    emits: ['options-change', 'change'],
+    setup(props, { emit }) {
+      const options = ref<OptionsItem[]>([]);
+      const loading = ref(false);
+      const isFirstLoad = ref(true);
+      const emitData = ref<any[]>([]);
+      const attrs = useAttrs();
+      const { t } = useI18n();
+      // Embedded in the form, just use the hook binding to perform form verification
+      const [state] = useRuleFormItem(props);
+
+      // Processing options value
+      const getOptions = computed(() => {
+        const { labelField, valueField, numberToString } = props;
+
+        return unref(options).reduce((prev, next: Recordable) => {
+          if (next) {
+            const value = next[valueField];
+            prev.push({
+              label: next[labelField],
+              value: numberToString ? `${value}` : value,
+              ...omit(next, [labelField, valueField]),
+            });
+          }
+          return prev;
+        }, [] as OptionsItem[]);
+      });
+
+      watchEffect(() => {
+        props.immediate && fetch();
+      });
+
+      watch(
+        () => props.params,
+        () => {
+          !unref(isFirstLoad) && fetch();
+        },
+        { deep: true },
+      );
+
+      async function fetch() {
+        const api = props.api;
+        if (!api || !isFunction(api)) return;
+        options.value = [];
+        try {
+          loading.value = true;
+          const res = await api(props.params);
+          if (Array.isArray(res)) {
+            options.value = res;
+            emitChange();
+            return;
+          }
+          if (props.resultField) {
+            options.value = get(res, props.resultField) || [];
+          }
+          emitChange();
+        } catch (error) {
+          console.warn(error);
+        } finally {
+          loading.value = false;
+        }
+      }
+
+      function emitChange() {
+        emit('options-change', unref(getOptions));
+      }
+
+      function handleChange(_, ...args) {
+        emitData.value = args;
+      }
+
+      return { state, getOptions, attrs, loading, t, handleChange, props };
+    },
+  });
+</script>

+ 12 - 7
src/components/Form/src/components/ApiSelect.vue

@@ -1,6 +1,6 @@
 <template>
   <Select
-    @dropdownVisibleChange="handleFetch"
+    @dropdown-visible-change="handleFetch"
     v-bind="$attrs"
     @change="handleChange"
     :options="getOptions"
@@ -57,6 +57,7 @@
       labelField: propTypes.string.def('label'),
       valueField: propTypes.string.def('value'),
       immediate: propTypes.bool.def(true),
+      alwaysLoad: propTypes.bool.def(false),
     },
     emits: ['options-change', 'change'],
     setup(props, { emit }) {
@@ -77,9 +78,9 @@
           if (next) {
             const value = next[valueField];
             prev.push({
+              ...omit(next, [labelField, valueField]),
               label: next[labelField],
               value: numberToString ? `${value}` : value,
-              ...omit(next, [labelField, valueField]),
             });
           }
           return prev;
@@ -87,7 +88,7 @@
       });
 
       watchEffect(() => {
-        props.immediate && fetch();
+        props.immediate && !props.alwaysLoad && fetch();
       });
 
       watch(
@@ -121,10 +122,14 @@
         }
       }
 
-      async function handleFetch() {
-        if (!props.immediate && unref(isFirstLoad)) {
-          await fetch();
-          isFirstLoad.value = false;
+      async function handleFetch(visible) {
+        if (visible) {
+          if (props.alwaysLoad) {
+            await fetch();
+          } else if (!props.immediate && unref(isFirstLoad)) {
+            await fetch();
+            isFirstLoad.value = false;
+          }
         }
       }
 

+ 90 - 0
src/components/Form/src/components/ApiTree.vue

@@ -0,0 +1,90 @@
+<template>
+  <a-tree v-bind="getAttrs" @change="handleChange">
+    <template #[item]="data" v-for="item in Object.keys($slots)">
+      <slot :name="item" v-bind="data || {}"></slot>
+    </template>
+    <template #suffixIcon v-if="loading">
+      <LoadingOutlined spin />
+    </template>
+  </a-tree>
+</template>
+
+<script lang="ts">
+  import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue';
+  import { Tree } from 'ant-design-vue';
+  import { isArray, isFunction } from '/@/utils/is';
+  import { get } from 'lodash-es';
+  import { propTypes } from '/@/utils/propTypes';
+  import { LoadingOutlined } from '@ant-design/icons-vue';
+  export default defineComponent({
+    name: 'ApiTree',
+    components: { ATree: Tree, LoadingOutlined },
+    props: {
+      api: { type: Function as PropType<(arg?: Recordable) => Promise<Recordable>> },
+      params: { type: Object },
+      immediate: { type: Boolean, default: true },
+      resultField: propTypes.string.def(''),
+      afterFetch: { type: Function as PropType<Fn> },
+    },
+    emits: ['options-change', 'change'],
+    setup(props, { attrs, emit }) {
+      const treeData = ref<Recordable[]>([]);
+      const isFirstLoaded = ref<Boolean>(false);
+      const loading = ref(false);
+      const getAttrs = computed(() => {
+        return {
+          ...(props.api ? { treeData: unref(treeData) } : {}),
+          ...attrs,
+        };
+      });
+
+      function handleChange(...args) {
+        emit('change', ...args);
+      }
+
+      watch(
+        () => props.params,
+        () => {
+          !unref(isFirstLoaded) && fetch();
+        },
+        { deep: true },
+      );
+
+      watch(
+        () => props.immediate,
+        (v) => {
+          v && !isFirstLoaded.value && fetch();
+        },
+      );
+
+      onMounted(() => {
+        props.immediate && fetch();
+      });
+
+      async function fetch() {
+        const { api, afterFetch } = props;
+        if (!api || !isFunction(api)) return;
+        loading.value = true;
+        treeData.value = [];
+        let result;
+        try {
+          result = await api(props.params);
+        } catch (e) {
+          console.error(e);
+        }
+        if (afterFetch && isFunction(afterFetch)) {
+          result = afterFetch(result);
+        }
+        loading.value = false;
+        if (!result) return;
+        if (!isArray(result)) {
+          result = get(result, props.resultField);
+        }
+        treeData.value = (result as Recordable[]) || [];
+        isFirstLoaded.value = true;
+        emit('options-change', treeData.value);
+      }
+      return { getAttrs, loading, handleChange };
+    },
+  });
+</script>

+ 1 - 1
src/components/Form/src/components/ApiTreeSelect.vue

@@ -44,7 +44,7 @@
       watch(
         () => props.params,
         () => {
-          isFirstLoaded.value && fetch();
+          !unref(isFirstLoaded) && fetch();
         },
         { deep: true },
       );

+ 20 - 8
src/components/Form/src/components/FormItem.vue

@@ -1,17 +1,16 @@
 <script lang="tsx">
   import type { PropType, Ref } from 'vue';
-  import type { FormActionType, FormProps } from '../types/form';
-  import type { FormSchema } from '../types/form';
+  import { computed, defineComponent, toRefs, unref } from 'vue';
+  import type { FormActionType, FormProps, FormSchema } from '../types/form';
   import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
   import type { TableActionType } from '/@/components/Table';
-  import { defineComponent, computed, unref, toRefs } from 'vue';
-  import { Form, Col, Divider } from 'ant-design-vue';
+  import { Col, Divider, Form } from 'ant-design-vue';
   import { componentMap } from '../componentMap';
   import { BasicHelp } from '/@/components/Basic';
   import { isBoolean, isFunction, isNull } from '/@/utils/is';
   import { getSlot } from '/@/utils/helper/tsxHelper';
   import { createPlaceholderMessage, setComponentRuleType } from '../helper';
-  import { upperFirst, cloneDeep } from 'lodash-es';
+  import { cloneDeep, upperFirst } from 'lodash-es';
   import { useItemLabelWidth } from '../hooks/useLabelWidth';
   import { useI18n } from '/@/hooks/web/useI18n';
 
@@ -178,8 +177,21 @@
 
         const getRequired = isFunction(required) ? required(unref(getValues)) : required;
 
-        if ((!rules || rules.length === 0) && getRequired) {
-          rules = [{ required: getRequired, validator }];
+        /*
+         * 1、若设置了required属性,又没有其他的rules,就创建一个验证规则;
+         * 2、若设置了required属性,又存在其他的rules,则只rules中不存在required属性时,才添加验证required的规则
+         *     也就是说rules中的required,优先级大于required
+         */
+        if (getRequired) {
+          if (!rules || rules.length === 0) {
+            rules = [{ required: getRequired, validator }];
+          } else {
+            const requiredIndex: number = rules.findIndex((rule) => Reflect.has(rule, 'required'));
+
+            if (requiredIndex === -1) {
+              rules.push({ required: getRequired, validator });
+            }
+          }
         }
 
         const requiredRuleIndex: number = rules.findIndex(
@@ -340,7 +352,7 @@
               wrapperCol={wrapperCol}
             >
               <div style="display:flex">
-                <div style="flex:1">{getContent()}</div>
+                <div style="flex:1;">{getContent()}</div>
                 {showSuffix && <span class="suffix">{getSuffix}</span>}
               </div>
             </Form.Item>

+ 2 - 0
src/components/Form/src/helper.ts

@@ -70,3 +70,5 @@ export function handleInputNumberValue(component?: ComponentType, val?: any) {
  * 时间字段
  */
 export const dateItemType = genType();
+
+export const defaultValueComponents = ['Input', 'InputPassword', 'InputSearch', 'InputTextArea'];

+ 37 - 9
src/components/Form/src/hooks/useFormEvents.ts

@@ -1,10 +1,10 @@
 import type { ComputedRef, Ref } from 'vue';
 import type { FormProps, FormSchema, FormActionType } from '../types/form';
 import type { NamePath } from 'ant-design-vue/lib/form/interface';
-import { unref, toRaw } from 'vue';
-import { isArray, isFunction, isObject, isString } from '/@/utils/is';
+import { unref, toRaw, nextTick } from 'vue';
+import { isArray, isFunction, isNullOrUnDef, isObject, isString } from '/@/utils/is';
 import { deepMerge } from '/@/utils';
-import { dateItemType, handleInputNumberValue } from '../helper';
+import { dateItemType, handleInputNumberValue, defaultValueComponents } from '../helper';
 import { dateUtil } from '/@/utils/dateUtil';
 import { cloneDeep, uniqBy } from 'lodash-es';
 import { error } from '/@/utils/log';
@@ -37,9 +37,12 @@ export function useFormEvents({
     if (!formEl) return;
 
     Object.keys(formModel).forEach((key) => {
-      formModel[key] = defaultValueRef.value[key];
+      const schema = unref(getSchema).find((item) => item.field === key);
+      const isInput = schema?.component && defaultValueComponents.includes(schema.component);
+      formModel[key] = isInput ? defaultValueRef.value[key] || '' : defaultValueRef.value[key];
     });
-    clearValidate();
+    nextTick(() => clearValidate());
+
     emit('reset', toRaw(formModel));
     submitOnReset && handleSubmit();
   }
@@ -125,18 +128,18 @@ export function useFormEvents({
     const schemaList: FormSchema[] = cloneDeep(unref(getSchema));
 
     const index = schemaList.findIndex((schema) => schema.field === prefixField);
-    const hasInList = schemaList.some((item) => item.field === prefixField || schema.field);
-
-    if (!hasInList) return;
 
     if (!prefixField || index === -1 || first) {
       first ? schemaList.unshift(schema) : schemaList.push(schema);
       schemaRef.value = schemaList;
+      _setDefaultValue(schema);
       return;
     }
     if (index !== -1) {
       schemaList.splice(index + 1, 0, schema);
     }
+    _setDefaultValue(schema);
+
     schemaRef.value = schemaList;
   }
 
@@ -192,9 +195,34 @@ export function useFormEvents({
         }
       });
     });
+    _setDefaultValue(schema);
+
     schemaRef.value = uniqBy(schema, 'field');
   }
 
+  function _setDefaultValue(data: FormSchema | FormSchema[]) {
+    let schemas: FormSchema[] = [];
+    if (isObject(data)) {
+      schemas.push(data as FormSchema);
+    }
+    if (isArray(data)) {
+      schemas = [...data];
+    }
+
+    const obj: Recordable = {};
+    schemas.forEach((item) => {
+      if (
+        item.component != 'Divider' &&
+        Reflect.has(item, 'field') &&
+        item.field &&
+        !isNullOrUnDef(item.defaultValue)
+      ) {
+        obj[item.field] = item.defaultValue;
+      }
+    });
+    setFieldsValue(obj);
+  }
+
   function getFieldsValue(): Recordable {
     const formEl = unref(formElRef);
     if (!formEl) return {};
@@ -242,7 +270,7 @@ export function useFormEvents({
       const values = await validate();
       const res = handleFormValues(values);
       emit('submit', res);
-    } catch (error) {
+    } catch (error: any) {
       throw new Error(error);
     }
   }

+ 47 - 3
src/components/Form/src/hooks/useFormValues.ts

@@ -11,6 +11,43 @@ interface UseFormValuesContext {
   getProps: ComputedRef<FormProps>;
   formModel: Recordable;
 }
+
+/**
+ * @desription deconstruct array-link key. This method will mutate the target.
+ */
+function tryDeconstructArray(key: string, value: any, target: Recordable) {
+  const pattern = /^\[(.+)\]$/;
+  if (pattern.test(key)) {
+    const match = key.match(pattern);
+    if (match && match[1]) {
+      const keys = match[1].split(',');
+      value = Array.isArray(value) ? value : [value];
+      keys.forEach((k, index) => {
+        set(target, k.trim(), value[index]);
+      });
+      return true;
+    }
+  }
+}
+
+/**
+ * @desription deconstruct object-link key. This method will mutate the target.
+ */
+function tryDeconstructObject(key: string, value: any, target: Recordable) {
+  const pattern = /^\{(.+)\}$/;
+  if (pattern.test(key)) {
+    const match = key.match(pattern);
+    if (match && match[1]) {
+      const keys = match[1].split(',');
+      value = isObject(value) ? value : {};
+      keys.forEach((k) => {
+        set(target, k.trim(), value[k.trim()]);
+      });
+      return true;
+    }
+  }
+}
+
 export function useFormValues({
   defaultValueRef,
   getSchema,
@@ -33,14 +70,18 @@ export function useFormValues({
       if (isObject(value)) {
         value = transformDateFunc?.(value);
       }
-      if (isArray(value) && value[0]?._isAMomentObject && value[1]?._isAMomentObject) {
+
+      if (isArray(value) && value[0]?.format && value[1]?.format) {
         value = value.map((item) => transformDateFunc?.(item));
       }
       // Remove spaces
       if (isString(value)) {
         value = value.trim();
       }
-      set(res, key, value);
+      if (!tryDeconstructArray(key, value, res) && !tryDeconstructObject(key, value, res)) {
+        // 没有解构成功的,按原样赋值
+        set(res, key, value);
+      }
     }
     return handleRangeTimeValue(res);
   }
@@ -77,7 +118,10 @@ export function useFormValues({
       const { defaultValue } = item;
       if (!isNullOrUnDef(defaultValue)) {
         obj[item.field] = defaultValue;
-        formModel[item.field] = defaultValue;
+
+        if (formModel[item.field] === undefined) {
+          formModel[item.field] = defaultValue;
+        }
       }
     });
     defaultValueRef.value = obj;

+ 6 - 3
src/components/Form/src/hooks/useLabelWidth.ts

@@ -1,7 +1,6 @@
 import type { Ref } from 'vue';
-import type { FormProps, FormSchema } from '../types/form';
-
 import { computed, unref } from 'vue';
+import type { FormProps, FormSchema } from '../types/form';
 import { isNumber } from '/@/utils/is';
 
 export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<FormProps>) {
@@ -14,6 +13,7 @@ export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<
       labelWidth: globalLabelWidth,
       labelCol: globalLabelCol,
       wrapperCol: globWrapperCol,
+      layout,
     } = unref(propsRef);
 
     // If labelWidth is set globally, all items setting
@@ -33,7 +33,10 @@ export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<
 
     return {
       labelCol: { style: { width }, ...col },
-      wrapperCol: { style: { width: `calc(100% - ${width})` }, ...wrapCol },
+      wrapperCol: {
+        style: { width: layout === 'vertical' ? '100%' : `calc(100% - ${width})` },
+        ...wrapCol,
+      },
     };
   });
 }

+ 2 - 1
src/components/Form/src/props.ts

@@ -40,6 +40,7 @@ export const basicProps = {
   // 在INPUT组件上单击回车时,是否自动提交
   autoSubmitOnEnter: propTypes.bool.def(false),
   submitOnReset: propTypes.bool,
+  submitOnChange: propTypes.bool,
   size: propTypes.oneOf(['default', 'small', 'large']).def('default'),
   // 禁用表单
   disabled: propTypes.bool,
@@ -53,7 +54,7 @@ export const basicProps = {
   transformDateFunc: {
     type: Function as PropType<Fn>,
     default: (date: any) => {
-      return date._isAMomentObject ? date?.format('YYYY-MM-DD HH:mm:ss') : date;
+      return date?.format?.('YYYY-MM-DD HH:mm:ss') ?? date;
     },
   },
   rulesMessageJoinLabel: propTypes.bool.def(true),

+ 6 - 3
src/components/Form/src/types/form.ts

@@ -49,17 +49,20 @@ export type RegisterFn = (formInstance: FormActionType) => void;
 export type UseFormReturnType = [RegisterFn, FormActionType];
 
 export interface FormProps {
+  name?: string;
   layout?: 'vertical' | 'inline' | 'horizontal';
   // Form value
   model?: Recordable;
   // The width of all items in the entire form
   labelWidth?: number | string;
-  //alignment
+  // alignment
   labelAlign?: 'left' | 'right';
-  //Row configuration for the entire form
+  // Row configuration for the entire form
   rowProps?: RowProps;
   // Submit form on reset
   submitOnReset?: boolean;
+  // Submit form on form changing
+  submitOnChange?: boolean;
   // Col configuration for the entire form
   labelCol?: Partial<ColEx>;
   // Col configuration for the entire form
@@ -129,7 +132,7 @@ export interface FormSchema {
   // Variable name bound to v-model Default value
   valueField?: string;
   // Label name
-  label: string;
+  label: string | VNode;
   // Auxiliary text
   subLabel?: string;
   // Help text on the right side of the text

+ 3 - 0
src/components/Form/src/types/index.ts

@@ -91,12 +91,15 @@ export type ComponentType =
   | 'Select'
   | 'ApiSelect'
   | 'TreeSelect'
+  | 'ApiTree'
   | 'ApiTreeSelect'
+  | 'ApiRadioGroup'
   | 'RadioButtonGroup'
   | 'RadioGroup'
   | 'Checkbox'
   | 'CheckboxGroup'
   | 'AutoComplete'
+  | 'ApiCascader'
   | 'Cascader'
   | 'DatePicker'
   | 'MonthPicker'

+ 73 - 98
src/components/Icon/src/IconPicker.vue

@@ -7,7 +7,7 @@
     v-model:value="currentSelect"
   >
     <template #addonAfter>
-      <Popover
+      <a-popover
         placement="bottomLeft"
         trigger="click"
         v-model="visible"
@@ -17,7 +17,7 @@
           <div class="flex justify-between">
             <a-input
               :placeholder="t('component.icon.search')"
-              @change="handleSearchChange"
+              @change="debounceHandleSearchChange"
               allowClear
             />
           </div>
@@ -31,18 +31,7 @@
                   v-for="icon in getPaginationList"
                   :key="icon"
                   :class="currentSelect === icon ? 'border border-primary' : ''"
-                  class="
-                    p-2
-                    w-1/8
-                    cursor-pointer
-                    mr-1
-                    mt-1
-                    flex
-                    justify-center
-                    items-center
-                    border border-solid
-                    hover:border-primary
-                  "
+                  class="p-2 w-1/8 cursor-pointer mr-1 mt-1 flex justify-center items-center border border-solid hover:border-primary"
                   @click="handleClick(icon)"
                   :title="icon"
                 >
@@ -53,7 +42,7 @@
               </ul>
             </ScrollContainer>
             <div class="flex py-2 items-center justify-center" v-if="getTotal >= pageSize">
-              <Pagination
+              <a-pagination
                 showLessItems
                 size="small"
                 :pageSize="pageSize"
@@ -63,7 +52,7 @@
             </div>
           </div>
           <template v-else
-            ><div class="p-5"><Empty /></div>
+            ><div class="p-5"><a-empty /></div>
           </template>
         </template>
 
@@ -71,16 +60,14 @@
           <SvgIcon :name="currentSelect" />
         </span>
         <Icon :icon="currentSelect || 'ion:apps-outline'" class="cursor-pointer px-2 py-1" v-else />
-      </Popover>
+      </a-popover>
     </template>
   </a-input>
 </template>
-<script lang="ts">
-  import { defineComponent, ref, watchEffect, watch, unref } from 'vue';
-
+<script lang="ts" setup>
+  import { ref, watchEffect, watch, unref } from 'vue';
   import { useDesign } from '/@/hooks/web/useDesign';
   import { ScrollContainer } from '/@/components/Container';
-
   import { Input, Popover, Pagination, Empty } from 'ant-design-vue';
   import Icon from './Icon.vue';
   import SvgIcon from './SvgIcon.vue';
@@ -94,6 +81,12 @@
   import { useMessage } from '/@/hooks/web/useMessage';
   import svgIcons from 'virtual:svg-icons-names';
 
+  // 没有使用别名引入,是因为WebStorm当前版本还不能正确识别,会报unused警告
+  const AInput = Input;
+  const APopover = Popover;
+  const APagination = Pagination;
+  const AEmpty = Empty;
+
   function getIcons() {
     const data = iconsData as any;
     const prefix: string = data?.prefix ?? '';
@@ -110,88 +103,70 @@
     return svgIcons.map((icon) => icon.replace('icon-', ''));
   }
 
-  export default defineComponent({
-    name: 'IconPicker',
-    components: { [Input.name]: Input, Icon, Popover, ScrollContainer, Pagination, Empty, SvgIcon },
-    inheritAttrs: false,
-    props: {
-      value: propTypes.string,
-      width: propTypes.string.def('100%'),
-      pageSize: propTypes.number.def(140),
-      copy: propTypes.bool.def(false),
-      mode: propTypes.oneOf<('svg' | 'iconify')[]>(['svg', 'iconify']).def('iconify'),
+  const props = defineProps({
+    value: propTypes.string,
+    width: propTypes.string.def('100%'),
+    pageSize: propTypes.number.def(140),
+    copy: propTypes.bool.def(false),
+    mode: propTypes.oneOf<('svg' | 'iconify')[]>(['svg', 'iconify']).def('iconify'),
+  });
+
+  const emit = defineEmits(['change', 'update:value']);
+
+  const isSvgMode = props.mode === 'svg';
+  const icons = isSvgMode ? getSvgIcons() : getIcons();
+
+  const currentSelect = ref('');
+  const visible = ref(false);
+  const currentList = ref(icons);
+
+  const { t } = useI18n();
+  const { prefixCls } = useDesign('icon-picker');
+
+  const debounceHandleSearchChange = useDebounceFn(handleSearchChange, 100);
+  const { clipboardRef, isSuccessRef } = useCopyToClipboard(props.value);
+  const { createMessage } = useMessage();
+
+  const { getPaginationList, getTotal, setCurrentPage } = usePagination(
+    currentList,
+    props.pageSize,
+  );
+
+  watchEffect(() => {
+    currentSelect.value = props.value;
+  });
+
+  watch(
+    () => currentSelect.value,
+    (v) => {
+      emit('update:value', v);
+      return emit('change', v);
     },
-    emits: ['change', 'update:value'],
-    setup(props, { emit }) {
-      const isSvgMode = props.mode === 'svg';
-      const icons = isSvgMode ? getSvgIcons() : getIcons();
-
-      const currentSelect = ref('');
-      const visible = ref(false);
-      const currentList = ref(icons);
-
-      const { t } = useI18n();
-      const { prefixCls } = useDesign('icon-picker');
-
-      const debounceHandleSearchChange = useDebounceFn(handleSearchChange, 100);
-      const { clipboardRef, isSuccessRef } = useCopyToClipboard(props.value);
-      const { createMessage } = useMessage();
-
-      const { getPaginationList, getTotal, setCurrentPage } = usePagination(
-        currentList,
-        props.pageSize,
-      );
-
-      watchEffect(() => {
-        currentSelect.value = props.value;
-      });
-
-      watch(
-        () => currentSelect.value,
-        (v) => {
-          emit('update:value', v);
-          return emit('change', v);
-        },
-      );
-
-      function handlePageChange(page: number) {
-        setCurrentPage(page);
-      }
+  );
 
-      function handleClick(icon: string) {
-        currentSelect.value = icon;
-        if (props.copy) {
-          clipboardRef.value = icon;
-          if (unref(isSuccessRef)) {
-            createMessage.success(t('component.icon.copy'));
-          }
-        }
-      }
+  function handlePageChange(page: number) {
+    setCurrentPage(page);
+  }
 
-      function handleSearchChange(e: ChangeEvent) {
-        const value = e.target.value;
-        if (!value) {
-          setCurrentPage(1);
-          currentList.value = icons;
-          return;
-        }
-        currentList.value = icons.filter((item) => item.includes(value));
+  function handleClick(icon: string) {
+    currentSelect.value = icon;
+    if (props.copy) {
+      clipboardRef.value = icon;
+      if (unref(isSuccessRef)) {
+        createMessage.success(t('component.icon.copy'));
       }
+    }
+  }
 
-      return {
-        t,
-        prefixCls,
-        visible,
-        isSvgMode,
-        getTotal,
-        getPaginationList,
-        handlePageChange,
-        handleClick,
-        currentSelect,
-        handleSearchChange: debounceHandleSearchChange,
-      };
-    },
-  });
+  function handleSearchChange(e: ChangeEvent) {
+    const value = e.target.value;
+    if (!value) {
+      setCurrentPage(1);
+      currentList.value = icons;
+      return;
+    }
+    currentList.value = icons.filter((item) => item.includes(value));
+  }
 </script>
 <style lang="less">
   @prefix-cls: ~'@{namespace}-icon-picker';

+ 15 - 3
src/components/Loading/src/Loading.vue

@@ -1,5 +1,10 @@
 <template>
-  <section class="full-loading" :class="{ absolute }" v-show="loading">
+  <section
+    class="full-loading"
+    :class="{ absolute, [theme]: !!theme }"
+    :style="[background ? `background-color: ${background}` : '']"
+    v-show="loading"
+  >
     <Spin v-bind="$attrs" :tip="tip" :size="size" :spinning="loading" />
   </section>
 </template>
@@ -35,6 +40,9 @@
       background: {
         type: String as PropType<string>,
       },
+      theme: {
+        type: String as PropType<'dark' | 'light'>,
+      },
     },
   });
 </script>
@@ -49,7 +57,7 @@
     height: 100%;
     justify-content: center;
     align-items: center;
-    background-color: rgba(240, 242, 245, 0.4);
+    background-color: rgb(240 242 245 / 40%);
 
     &.absolute {
       position: absolute;
@@ -60,8 +68,12 @@
   }
 
   html[data-theme='dark'] {
-    .full-loading {
+    .full-loading:not(.light) {
       background-color: @modal-mask-bg;
     }
   }
+
+  .full-loading.dark {
+    background-color: @modal-mask-bg;
+  }
 </style>

+ 2 - 1
src/components/Markdown/src/MarkdownViewer.vue

@@ -1,9 +1,10 @@
 <template>
+  <!-- eslint-disable vue/no-v-html -->
   <div v-html="getHtmlData" :class="$props.class" class="markdown-viewer"></div>
 </template>
 
 <script lang="ts" setup>
-  import { computed } from 'vue';
+  import { computed, defineProps } from 'vue';
   import showdown from 'showdown';
 
   const converter = new showdown.Converter();

+ 4 - 2
src/components/Menu/src/BasicMenu.vue

@@ -6,7 +6,7 @@
     :openKeys="getOpenKeys"
     :inlineIndent="inlineIndent"
     :theme="theme"
-    @openChange="handleOpenChange"
+    @open-change="handleOpenChange"
     :class="getMenuClass"
     @click="handleMenuClick"
     :subMenuOpenDelay="0.2"
@@ -134,7 +134,9 @@
           isClickGo.value = false;
           return;
         }
-        const path = (route || unref(currentRoute)).path;
+        const path =
+          (route || unref(currentRoute)).meta?.currentActiveMenu ||
+          (route || unref(currentRoute)).path;
         setOpenKeys(path);
         if (unref(currentActiveMenu)) return;
         if (props.isHorizontal && unref(getSplit)) {

+ 5 - 2
src/components/Modal/src/BasicModal.vue

@@ -1,5 +1,5 @@
 <template>
-  <Modal v-bind="getBindValue">
+  <Modal v-bind="getBindValue" @cancel="handleCancel">
     <template #closeIcon v-if="!$slots.closeIcon">
       <ModalClose
         :canFullscreen="getProps.canFullscreen"
@@ -72,6 +72,7 @@
   import { basicProps } from './props';
   import { useFullScreen } from './hooks/useModalFullScreen';
   import { omit } from 'lodash-es';
+  import { useDesign } from '/@/hooks/web/useDesign';
 
   export default defineComponent({
     name: 'BasicModal',
@@ -83,6 +84,7 @@
       const visibleRef = ref(false);
       const propsRef = ref<Partial<ModalProps> | null>(null);
       const modalWrapperRef = ref<any>(null);
+      const { prefixCls } = useDesign('basic-modal');
 
       // modal   Bottom and top height
       const extHeightRef = ref(0);
@@ -175,7 +177,8 @@
       // 取消事件
       async function handleCancel(e: Event) {
         e?.stopPropagation();
-
+        // 过滤自定义关闭按钮的空白区域
+        if ((e.target as HTMLElement)?.classList?.contains(prefixCls + '-close--custom')) return;
         if (props.closeFunc && isFunction(props.closeFunc)) {
           const isClose: boolean = await props.closeFunc();
           visibleRef.value = !isClose;

+ 7 - 2
src/components/Modal/src/components/Modal.tsx

@@ -9,7 +9,8 @@ export default defineComponent({
   name: 'Modal',
   inheritAttrs: false,
   props: basicProps,
-  setup(props, { slots }) {
+  emits: ['cancel'],
+  setup(props, { slots, emit }) {
     const { visible, draggable, destroyOnClose } = toRefs(props);
     const attrs = useAttrs();
     useModalDragMove({
@@ -18,8 +19,12 @@ export default defineComponent({
       draggable,
     });
 
+    const onCancel = (e: Event) => {
+      emit('cancel', e);
+    };
+
     return () => {
-      const propsData = { ...unref(attrs), ...props } as Recordable;
+      const propsData = { ...unref(attrs), ...props, onCancel } as Recordable;
       return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
     };
   },

+ 1 - 1
src/components/Modal/src/components/ModalClose.vue

@@ -97,7 +97,7 @@
       }
     }
 
-    & span:nth-child(2) {
+    & span:last-child {
       &:hover {
         color: @error-color;
       }

+ 1 - 0
src/components/Modal/src/components/ModalHeader.vue

@@ -17,5 +17,6 @@
       },
       title: { type: String },
     },
+    emits: ['dblclick'],
   });
 </script>

+ 4 - 1
src/components/Modal/src/index.less

@@ -54,7 +54,7 @@
   }
 
   &-content {
-    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+    box-shadow: 0 4px 8px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
   }
 
   &-footer {
@@ -111,16 +111,19 @@
 .ant-modal-confirm .ant-modal-body {
   padding: 24px !important;
 }
+
 @media screen and (max-height: 600px) {
   .ant-modal {
     top: 60px;
   }
 }
+
 @media screen and (max-height: 540px) {
   .ant-modal {
     top: 30px;
   }
 }
+
 @media screen and (max-height: 480px) {
   .ant-modal {
     top: 10px;

+ 2 - 2
src/components/Page/src/PageFooter.vue

@@ -39,8 +39,8 @@
     line-height: 44px;
     background-color: @component-background;
     border-top: 1px solid @border-color-base;
-    box-shadow: 0 -6px 16px -8px rgba(0, 0, 0, 0.08), 0 -9px 28px 0 rgba(0, 0, 0, 0.05),
-      0 -12px 48px 16px rgba(0, 0, 0, 0.03);
+    box-shadow: 0 -6px 16px -8px rgb(0 0 0 / 8%), 0 -9px 28px 0 rgb(0 0 0 / 5%),
+      0 -12px 48px 16px rgb(0 0 0 / 3%);
     transition: width 0.2s;
 
     &__left {

+ 6 - 1
src/components/Page/src/PageWrapper.vue

@@ -5,7 +5,7 @@
       :title="title"
       v-bind="omit($attrs, 'class')"
       ref="headerRef"
-      v-if="content || $slots.headerContent || title || getHeaderSlots.length"
+      v-if="getShowHeader"
     >
       <template #default>
         <template v-if="content">
@@ -99,6 +99,10 @@
         ];
       });
 
+      const getShowHeader = computed(
+        () => props.content || slots?.headerContent || props.title || getHeaderSlots.value.length,
+      );
+
       const getShowFooter = computed(() => slots?.leftFooter || slots?.rightFooter);
 
       const getHeaderSlots = computed(() => {
@@ -150,6 +154,7 @@
         getClass,
         getHeaderSlots,
         prefixCls,
+        getShowHeader,
         getShowFooter,
         omit,
         getContentClass,

+ 7 - 7
src/components/Preview/src/Functional.vue

@@ -432,7 +432,7 @@
     bottom: 0;
     left: 0;
     z-index: @preview-comp-z-index;
-    background: rgba(0, 0, 0, 0.5);
+    background: rgb(0 0 0 / 50%);
     user-select: none;
 
     &-content {
@@ -458,7 +458,7 @@
       overflow: hidden;
       color: @white;
       cursor: pointer;
-      background-color: rgba(0, 0, 0, 0.5);
+      background-color: rgb(0 0 0 / 50%);
       border-radius: 50%;
       transition: all 0.2s;
 
@@ -470,7 +470,7 @@
       }
 
       &:hover {
-        background-color: rgba(0, 0, 0, 0.8);
+        background-color: rgb(0 0 0 / 80%);
       }
     }
 
@@ -480,7 +480,7 @@
       left: 50%;
       padding: 0 22px;
       font-size: 16px;
-      background: rgba(109, 109, 109, 0.6);
+      background: rgb(109 109 109 / 60%);
       border-radius: 15px;
       transform: translateX(-50%);
     }
@@ -494,7 +494,7 @@
       height: 44px;
       padding: 0 22px;
       margin-left: -139px;
-      background: rgba(109, 109, 109, 0.6);
+      background: rgb(109 109 109 / 60%);
       border-radius: 22px;
       justify-content: center;
 
@@ -526,12 +526,12 @@
       height: 50px;
       font-size: 28px;
       cursor: pointer;
-      background-color: rgba(0, 0, 0, 0.5);
+      background-color: rgb(0 0 0 / 50%);
       border-radius: 50%;
       transition: all 0.2s;
 
       &:hover {
-        background-color: rgba(0, 0, 0, 0.8);
+        background-color: rgb(0 0 0 / 80%);
       }
 
       &.left {

+ 1 - 1
src/components/Preview/src/Preview.vue

@@ -88,7 +88,7 @@
     }
 
     .ant-image-preview-operations {
-      background-color: rgba(0, 0, 0, 0.4);
+      background-color: rgb(0 0 0 / 40%);
     }
   }
 </style>

+ 5 - 6
src/components/Scrollbar/src/Scrollbar.vue

@@ -148,7 +148,7 @@
           display: none;
           width: 0;
           height: 0;
-          opacity: 0;
+          opacity: 0%;
         }
       }
     }
@@ -159,12 +159,12 @@
       width: 0;
       height: 0;
       cursor: pointer;
-      background-color: rgba(144, 147, 153, 0.3);
+      background-color: rgb(144 147 153 / 30%);
       border-radius: inherit;
       transition: 0.3s background-color;
 
       &:hover {
-        background-color: rgba(144, 147, 153, 0.5);
+        background-color: rgb(144 147 153 / 50%);
       }
     }
 
@@ -174,8 +174,7 @@
       bottom: 2px;
       z-index: 1;
       border-radius: 4px;
-      opacity: 0;
-      -webkit-transition: opacity 80ms ease;
+      opacity: 0%;
       transition: opacity 80ms ease;
 
       &.is-vertical {
@@ -201,7 +200,7 @@
   .scrollbar:active > .scrollbar__bar,
   .scrollbar:focus > .scrollbar__bar,
   .scrollbar:hover > .scrollbar__bar {
-    opacity: 1;
+    opacity: 100%;
     transition: opacity 340ms ease-out;
   }
 </style>

+ 1 - 1
src/components/SimpleMenu/src/components/SubMenuItem.vue

@@ -21,7 +21,7 @@
       :overlayClassName="`${prefixCls}-menu-popover`"
       v-else
       :visible="getIsOpend"
-      @visibleChange="handleVisibleChange"
+      @visible-change="handleVisibleChange"
       :overlayStyle="getOverlayStyle"
       :align="{ offset: [0, 0] }"
     >

+ 7 - 7
src/components/SimpleMenu/src/components/menu.less

@@ -13,8 +13,8 @@
     bottom: 0;
     display: block;
     width: 2px;
-    background-color: @primary-color;
     content: '';
+    background-color: @primary-color;
   }
 }
 
@@ -45,8 +45,8 @@
         position: absolute;
         top: 50%;
         right: 18px;
-        transform: translateY(-50%) rotate(-90deg);
         transition: transform @transition-time @ease-in-out;
+        transform: translateY(-50%) rotate(-90deg);
       }
     }
 
@@ -128,12 +128,12 @@
       position: relative;
       z-index: 1;
       display: flex;
+      align-items: center;
       font-size: @font-size-base;
       color: inherit;
       list-style: none;
       cursor: pointer;
       outline: none;
-      align-items: center;
 
       &:hover,
       &:active {
@@ -178,8 +178,8 @@
     &-vertical &-submenu-collapse {
       .@{submenu-popup-prefix-cls} {
         display: flex;
-        justify-content: center;
         align-items: center;
+        justify-content: center;
       }
       .@{menu-prefix-cls}-submenu-collapsed-show-tit {
         flex-direction: column;
@@ -188,7 +188,7 @@
 
     &-vertical&-collapse &-item,
     &-vertical&-collapse &-submenu-title {
-      padding: 0 0;
+      padding: 0;
     }
 
     &-vertical &-submenu-title-icon {
@@ -244,8 +244,8 @@
           left: 0;
           width: 3px;
           height: 100%;
-          background-color: @primary-color;
           content: '';
+          background-color: @primary-color;
         }
       }
     }
@@ -276,8 +276,8 @@
           left: 0;
           width: 3px;
           height: 100%;
-          background-color: @primary-color;
           content: '';
+          background-color: @primary-color;
         }
 
         .@{menu-prefix-cls}-submenu-collapse {

+ 1 - 1
src/components/StrengthMeter/src/StrengthMeter.vue

@@ -92,7 +92,7 @@
         background-color: transparent;
         border-color: @white;
         border-style: solid;
-        border-width: 0 5px 0 5px;
+        border-width: 0 5px;
         content: '';
       }
 

Some files were not shown because too many files changed in this diff