陈文彬 vor 4 Jahren
Commit
2f6253cfb6
100 geänderte Dateien mit 2858 neuen und 0 gelöschten Zeilen
  1. 15 0
      .editorconfig
  2. 0 0
      .env
  3. 1 0
      .env.development
  4. 1 0
      .env.production
  5. 28 0
      .eslintignore
  6. 62 0
      .eslintrc.js
  7. 6 0
      .gitignore
  8. 20 0
      .ls-lint.yml
  9. 7 0
      .prettierignore
  10. 21 0
      CHANGELOG.md
  11. 21 0
      LICENSE
  12. 26 0
      build/config/glob/lessModifyVars.ts
  13. 10 0
      build/config/vite/env.ts
  14. 15 0
      build/config/vite/proxy.ts
  15. 22 0
      build/gzip/index.ts
  16. 56 0
      build/gzip/types.ts
  17. 39 0
      build/script/changelog.ts
  18. 10 0
      build/script/postinstall.ts
  19. 67 0
      build/script/preserve.ts
  20. 70 0
      build/script/preview.ts
  21. 18 0
      build/tsconfig.json
  22. 87 0
      build/utils.ts
  23. 55 0
      commitlint.config.js
  24. 13 0
      index.html
  25. 8 0
      lint-staged.config.js
  26. 7 0
      mock/_createProductionServer.ts
  27. 38 0
      mock/_util.ts
  28. 132 0
      mock/sys/menu.ts
  29. 90 0
      mock/sys/user.ts
  30. 104 0
      package.json
  31. 4 0
      postcss.config.js
  32. 28 0
      prettier.config.js
  33. BIN
      public/favicon.ico
  34. 42 0
      src/App.vue
  35. 18 0
      src/api/sys/menu.ts
  36. 23 0
      src/api/sys/model/menuModel.ts
  37. 43 0
      src/api/sys/model/userModel.ts
  38. 48 0
      src/api/sys/user.ts
  39. BIN
      src/assets/images/dashboard/wokb/approve.png
  40. BIN
      src/assets/images/dashboard/wokb/attendance.png
  41. BIN
      src/assets/images/dashboard/wokb/datashow1.png
  42. BIN
      src/assets/images/dashboard/wokb/datashow2.png
  43. BIN
      src/assets/images/dashboard/wokb/datashow3.png
  44. BIN
      src/assets/images/dashboard/wokb/datashow4.png
  45. BIN
      src/assets/images/dashboard/wokb/leave.png
  46. BIN
      src/assets/images/dashboard/wokb/meal.png
  47. BIN
      src/assets/images/dashboard/wokb/overtime.png
  48. BIN
      src/assets/images/dashboard/wokb/performance.png
  49. BIN
      src/assets/images/dashboard/wokb/stamp.png
  50. BIN
      src/assets/images/dashboard/wokb/travel.png
  51. BIN
      src/assets/images/dashboard/wokb/wokb.png
  52. BIN
      src/assets/images/exception/404.png
  53. BIN
      src/assets/images/exception/500.png
  54. BIN
      src/assets/images/exception/net-work.png
  55. BIN
      src/assets/images/header.jpg
  56. 39 0
      src/assets/images/layout/menu-mix.svg
  57. 39 0
      src/assets/images/layout/menu-sidebar.svg
  58. 39 0
      src/assets/images/layout/menu-top.svg
  59. 67 0
      src/assets/images/loading.svg
  60. BIN
      src/assets/images/lock-page.jpg
  61. BIN
      src/assets/images/lock-page.png
  62. BIN
      src/assets/images/login/login-bg.png
  63. BIN
      src/assets/images/login/login-in.png
  64. BIN
      src/assets/images/logo.png
  65. BIN
      src/assets/images/no-data.png
  66. BIN
      src/assets/images/page_null.png
  67. BIN
      src/assets/images/qq.jpeg
  68. BIN
      src/assets/images/sidebar/dark-mini.png
  69. BIN
      src/assets/images/sidebar/dark.png
  70. BIN
      src/assets/images/sidebar/light-mini.png
  71. BIN
      src/assets/images/sidebar/light.png
  72. 0 0
      src/assets/svg/preview/p-rotate.svg
  73. 1 0
      src/assets/svg/preview/resume.svg
  74. 1 0
      src/assets/svg/preview/scale.svg
  75. 0 0
      src/assets/svg/preview/unrotate.svg
  76. 1 0
      src/assets/svg/preview/unscale.svg
  77. 59 0
      src/components/Authority/index.tsx
  78. 4 0
      src/components/Basic/index.ts
  79. 53 0
      src/components/Basic/src/BasicArrow.vue
  80. 28 0
      src/components/Basic/src/BasicEmpty.vue
  81. 18 0
      src/components/Basic/src/BasicHelp.less
  82. 107 0
      src/components/Basic/src/BasicHelp.tsx
  83. 58 0
      src/components/Basic/src/BasicTitle.vue
  84. 100 0
      src/components/Breadcrumb/Breadcrumb.vue
  85. 62 0
      src/components/Breadcrumb/BreadcrumbItem.vue
  86. 88 0
      src/components/Button/index.vue
  87. 66 0
      src/components/Button/types.ts
  88. 21 0
      src/components/ClickOutSide/index.vue
  89. 5 0
      src/components/Container/index.ts
  90. 27 0
      src/components/Container/src/LazyContainer.less
  91. 200 0
      src/components/Container/src/LazyContainer.tsx
  92. 80 0
      src/components/Container/src/ScrollContainer.vue
  93. 117 0
      src/components/Container/src/collapse/CollapseContainer.vue
  94. 32 0
      src/components/Container/src/collapse/CollapseHeader.vue
  95. 17 0
      src/components/Container/src/types.d.ts
  96. 65 0
      src/components/ContextMenu/index.ts
  97. 49 0
      src/components/ContextMenu/src/index.less
  98. 90 0
      src/components/ContextMenu/src/index.tsx
  99. 40 0
      src/components/ContextMenu/src/props.ts
  100. 30 0
      src/components/ContextMenu/src/types.ts

+ 15 - 0
.editorconfig

@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset=utf-8
+end_of_line=lf
+insert_final_newline=false
+indent_style=space
+indent_size=2
+
+[*.yml]
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false

+ 0 - 0
.env


+ 1 - 0
.env.development

@@ -0,0 +1 @@
+VITE_USE_MOCK=true

+ 1 - 0
.env.production

@@ -0,0 +1 @@
+VITE_USE_MOCK=true

+ 28 - 0
.eslintignore

@@ -0,0 +1,28 @@
+
+*.sh
+node_modules
+lib
+*.md
+*.scss
+*.woff
+*.ttf
+.vscode
+.idea
+/dist/
+/mock/
+/public
+/docs
+.vscode
+.local
+/bin
+/build
+/config
+Dockerfile
+vue.config.js
+commit-lint.js
+/src/assets/iconfont/
+/types/shims
+/src/types/shims
+postcss.config.js
+stylelint.config.js
+commitlint.config.js

+ 62 - 0
.eslintrc.js

@@ -0,0 +1,62 @@
+module.exports = {
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    parser: '@typescript-eslint/parser',
+    ecmaVersion: 2020,
+    sourceType: 'module',
+    ecmaFeatures: {
+      jsx: true,
+      jsx: true,
+    },
+  },
+
+  extends: [
+    'plugin:vue/vue3-recommended',
+    'plugin:@typescript-eslint/recommended',
+    'prettier/@typescript-eslint',
+    'plugin:prettier/recommended',
+  ],
+  rules: {
+    '@typescript-eslint/ban-ts-ignore': 'off',
+    '@typescript-eslint/explicit-function-return-type': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/no-var-requires': 'off',
+    '@typescript-eslint/no-empty-function': 'off',
+    'vue/custom-event-name-casing': 'off',
+    'no-use-before-define': 'off',
+    // 'no-use-before-define': [
+    //   'error',
+    //   {
+    //     functions: false,
+    //     classes: true,
+    //   },
+    // ],
+    '@typescript-eslint/no-use-before-define': 'off',
+    // '@typescript-eslint/no-use-before-define': [
+    //   'error',
+    //   {
+    //     functions: false,
+    //     classes: true,
+    //   },
+    // ],
+    '@typescript-eslint/ban-ts-comment': 'off',
+    '@typescript-eslint/ban-types': 'off',
+    '@typescript-eslint/no-non-null-assertion': 'off',
+    '@typescript-eslint/explicit-module-boundary-types': 'off',
+    '@typescript-eslint/no-unused-vars': [
+      'error',
+      {
+        argsIgnorePattern: '^h$',
+        varsIgnorePattern: '^h$',
+      },
+    ],
+    'no-unused-vars': [
+      'error',
+      {
+        argsIgnorePattern: '^h$',
+        varsIgnorePattern: '^h$',
+      },
+    ],
+    'space-before-function-paren': 'off',
+  },
+};

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+node_modules
+.DS_Store
+dist
+*.local
+.npmrc
+.cache

+ 20 - 0
.ls-lint.yml

@@ -0,0 +1,20 @@
+ls:
+  src/*:
+    .js: kebab-case | PascalCase
+    .vue: PascalCase | regex:^index
+    .ts: camelCase | PascalCase
+    .d.ts: kebab-case
+    .mock.ts: kebab-case
+    .data.ts: camelCase | kebab-case
+    .test-d.ts: kebab-case
+    .less: kebab-case | PascalCase
+    .spec.ts: camelCase | PascalCase
+
+ignore:
+  - node_modules
+  - .git
+  - .circleci
+  - .github
+  - .vscode
+  - dist
+  - .local

+ 7 - 0
.prettierignore

@@ -0,0 +1,7 @@
+/dist/*
+.local
+.output.js
+/node_modules/**
+
+**/*.svg
+**/*.sh

+ 21 - 0
CHANGELOG.md

@@ -0,0 +1,21 @@
+# 2.0.0 (2020-09-28)
+
+### Features
+
+- add 37afeff
+- add menu ab58829
+- add menu aeb75e7
+- add split menu 6b2b7bd
+- add split menu 2e7cb0b
+- add split menu 58fa70e
+- add split menu aa87a2c
+- auth d36878e
+- header be36cc2
+- prettier 3f1db50
+
+### Performance Improvements
+
+- form 2f94a5d
+- loading 788fd64
+- lockpage 92d6b7e
+- menu ae6ace8

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020-present, Vben
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 26 - 0
build/config/glob/lessModifyVars.ts

@@ -0,0 +1,26 @@
+/**
+ * less global variable
+ */
+const primaryColor = '#018ffb';
+//{
+const modifyVars = {
+  'primary-color': primaryColor, //  Global dominant color
+  'info-color': primaryColor, //  Default color
+  'success-color': '#55D187', //  Success color
+  'error-color': '#ED6F6F', //  False color
+  'warning-color': '#EFBD47', //   Warning color
+  'link-color': primaryColor, //   Link color
+  'disabled-color': '#C2C2CC', //  Failure color
+  'heading-color': '#2C3A61', //  Title color
+  'text-color': '#2C3A61', //  Main text color
+  'text-color-secondary ': '#606266', // Subtext color
+  'background-color-base': '#F0F2F5', // background color
+  'font-size-base': '14px', //  Main font size
+  'box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)', //  Floating shadow
+  'border-color-base': '#cececd', //  Border color,
+  'border-color-split': '#cececd', //  Border color,
+  'border-radius-base': '2px', //  Component/float fillet
+};
+//}
+
+export { modifyVars, primaryColor };

+ 10 - 0
build/config/vite/env.ts

@@ -0,0 +1,10 @@
+import moment from 'moment';
+// @ts-ignore
+import pkg from '../../../package.json';
+export function setupBasicEnv() {
+  // version
+  process.env.VITE_VERSION = (pkg as any).version;
+  // build time
+  process.env.VITE_APP_BUILD_TIME = moment().format('YYYY-MM-DD HH:mm:ss');
+  process.env.VITE_BUILD_SHORT_TIME = moment().format('MMDDHHmmss');
+}

+ 15 - 0
build/config/vite/proxy.ts

@@ -0,0 +1,15 @@
+type ProxyItem = [string, string];
+
+type ProxyList = ProxyItem[];
+
+export function createProxy(list: ProxyList) {
+  const ret: any = {};
+  for (const [prefix, target] of list) {
+    ret[prefix] = {
+      target: target,
+      changeOrigin: true,
+      rewrite: (path: string) => path.replace(new RegExp(`^${prefix}`), ''),
+    };
+  }
+  return ret;
+}

+ 22 - 0
build/gzip/index.ts

@@ -0,0 +1,22 @@
+// Build gzip after packaging
+// import { readFile, writeFile } from 'fs';
+import viteConfig from '../../vite.config';
+import {
+  // basename,
+  join,
+} from 'path';
+// import { promisify } from 'util';
+// import { gzip, ZlibOptions } from 'zlib';
+import { readAllFile } from '../utils';
+
+// const readFilePromise = promisify(readFile);
+// const writeFilePromise = promisify(writeFile);
+
+// function createGzip() {}
+
+const FILE_REG = /\.(js|mjs|json|css|html)$/;
+
+const OUT_DIR = viteConfig.outDir || 'dist';
+
+// TODO 待开发
+const files = readAllFile(join(process.cwd(), OUT_DIR), FILE_REG);

+ 56 - 0
build/gzip/types.ts

@@ -0,0 +1,56 @@
+import type { ZlibOptions } from 'zlib';
+
+export type StringMappingOption = (originalString: string) => string;
+export type CustomCompressionOption = (
+  content: string | Buffer
+) => string | Buffer | Promise<string | Buffer>;
+
+export interface GzipPluginOptions {
+  /**
+   * Control which of the output files to compress
+   *
+   * Defaults to `/\.(js|mjs|json|css|html)$/`
+   */
+  filter?: RegExp | ((fileName: string) => boolean);
+
+  /**
+   * GZIP compression options, see https://nodejs.org/api/zlib.html#zlib_class_options
+   */
+  gzipOptions?: ZlibOptions;
+
+  /**
+   * Specified the minimum size in Bytes for a file to get compressed.
+   * Files that are smaller than this threshold will not be compressed.
+   * This does not apply to the files specified through `additionalFiles`!
+   */
+  minSize?: number;
+
+  /**
+   * This option allows you to compress additional files outside of the main rollup bundling process.
+   * The processing is delayed to make sure the files are written on disk; the delay is controlled
+   * through `additionalFilesDelay`.
+   */
+  additionalFiles?: string[];
+
+  /**
+   * This options sets a delay (ms) before the plugin compresses the files specified through `additionalFiles`.
+   * Increase this value if your artifacts take a long time to generate.
+   *
+   * Defaults to `2000`
+   */
+  additionalFilesDelay?: number;
+
+  /**
+   * Set a custom compression algorithm. The function can either return the compressed contents synchronously,
+   * or otherwise return a promise for asynchronous processing.
+   */
+  customCompression?: CustomCompressionOption;
+
+  /**
+   * Set a custom file name convention for the compressed files. Can be a suffix string or a function
+   * returning the file name.
+   *
+   * Defaults to `.gz`
+   */
+  fileName?: string | StringMappingOption;
+}

+ 39 - 0
build/script/changelog.ts

@@ -0,0 +1,39 @@
+// #!/usr/bin/env node
+
+import { sh } from 'tasksfile';
+import chalk from 'chalk';
+
+const createChangeLog = async () => {
+  try {
+    let cmd = `conventional-changelog -p angular -i CHANGELOG.md -s -r 0 `;
+    // let cmd = `conventional-changelog -p angular -i CHANGELOG.md -s -r 0 `;
+    // if (shell.which('git')) {
+    //   cmd += '&& git add CHANGELOG.md';
+    // }
+    await sh(cmd, {
+      async: true,
+      nopipe: true,
+    });
+
+    await sh('prettier --write **/CHANGELOG.md ', {
+      async: true,
+      nopipe: true,
+    });
+    console.log(
+      chalk.blue.bold('****************  ') +
+        chalk.green.bold('CHANGE_LOG generated successfully!') +
+        chalk.blue.bold('  ****************')
+    );
+  } catch (error) {
+    console.log(
+      chalk.blue.red('****************  ') +
+        chalk.green.red('CHANGE_LOG generated error\n' + error) +
+        chalk.blue.red('  ****************')
+    );
+    process.exit(1);
+  }
+};
+createChangeLog();
+module.exports = {
+  createChangeLog,
+};

+ 10 - 0
build/script/postinstall.ts

@@ -0,0 +1,10 @@
+import { exec, which } from 'shelljs';
+
+function ignoreCaseGit() {
+  try {
+    if (which('git')) {
+      exec('git config core.ignorecase false ');
+    }
+  } catch (error) {}
+}
+ignoreCaseGit();

+ 67 - 0
build/script/preserve.ts

@@ -0,0 +1,67 @@
+// 是否需要更新依赖,防止package.json更新了依赖,其他人获取代码后没有install
+
+import path from 'path';
+import fs from 'fs-extra';
+import { isEqual } from 'lodash';
+import chalk from 'chalk';
+import { sh } from 'tasksfile';
+
+const resolve = (dir: string) => {
+  return path.resolve(process.cwd(), dir);
+};
+
+let NEED_INSTALL = false;
+
+fs.mkdirp(resolve('build/.cache'));
+function checkPkgUpdate() {
+  const pkg = require('../../package.json');
+  const { dependencies, devDependencies } = pkg;
+  const depsFile = resolve('build/.cache/deps.json');
+  if (!fs.pathExistsSync(depsFile)) {
+    NEED_INSTALL = true;
+    return;
+  }
+  const depsJson = require('../.cache/deps.json');
+
+  if (!isEqual(depsJson, { dependencies, devDependencies })) {
+    NEED_INSTALL = true;
+  }
+}
+checkPkgUpdate();
+
+(async () => {
+  if (NEED_INSTALL) {
+    console.log(
+      chalk.blue.bold('****************  ') +
+        chalk.red.bold('检测到依赖变化,正在安装依赖(Tip: 项目首次运行也会执行)!') +
+        chalk.blue.bold('  ****************')
+    );
+    try {
+      // 从代码执行貌似不会自动读取.npmrc 所以手动加上源地址
+      // await run('yarn install --registry=https://registry.npm.taobao.org ', {
+      await sh('yarn install ', {
+        async: true,
+        nopipe: true,
+      });
+      console.log(
+        chalk.blue.bold('****************  ') +
+          chalk.green.bold('依赖安装成功,正在运行!') +
+          chalk.blue.bold('  ****************')
+      );
+
+      const pkg = require('../../package.json');
+      const { dependencies, devDependencies } = pkg;
+      const depsFile = resolve('build/.cache/deps.json');
+      const deps = { dependencies, devDependencies };
+      if (!fs.pathExistsSync(depsFile)) {
+        fs.writeFileSync(depsFile, JSON.stringify(deps));
+      } else {
+        const depsFile = resolve('build/.cache/deps.json');
+        const depsJson = require('../.cache/deps.json');
+        if (!isEqual(depsJson, deps)) {
+          fs.writeFileSync(depsFile, JSON.stringify(deps));
+        }
+      }
+    } catch (error) {}
+  }
+})();

+ 70 - 0
build/script/preview.ts

@@ -0,0 +1,70 @@
+import chalk from 'chalk';
+import Koa from 'koa';
+import inquirer from 'inquirer';
+import { sh } from 'tasksfile';
+import staticServer from 'koa-static';
+import portfinder from 'portfinder';
+import { resolve } from 'path';
+import viteConfig from '../../vite.config';
+import { getIPAddress } from '../utils';
+
+const BUILD = 1;
+const NO_BUILD = 2;
+
+// 启动服务器
+const startApp = () => {
+  const port = 9680;
+  portfinder.basePort = port;
+  const app = new Koa();
+  // const connect = require('connect');
+  // const serveStatic = require('serve-static');
+  // const app = connect();
+
+  app.use(staticServer(resolve(process.cwd(), viteConfig.outDir || 'dist')));
+
+  portfinder.getPort(async (err, port) => {
+    if (err) {
+      throw err;
+    } else {
+      // const publicPath = process.env.BASE_URL;
+      app.listen(port, function () {
+        const empty = '    ';
+        const common = `The preview program is already running:
+    - LOCAL: http://localhost:${port}/
+    - NETWORK: http://${getIPAddress()}:${port}/
+    `;
+        console.log(chalk.cyan('\n' + empty + common));
+      });
+    }
+  });
+};
+
+const preview = async () => {
+  const prompt = inquirer.prompt({
+    type: 'list',
+    message: 'Please select a preview method',
+    name: 'type',
+    choices: [
+      {
+        name: 'Preview after packaging',
+        value: BUILD,
+      },
+      {
+        name: `No packaging, preview directly (need to have dist file after packaging)`,
+        value: NO_BUILD,
+      },
+    ],
+  });
+  const { type } = await prompt;
+  if (type === BUILD) {
+    await sh('npm run build', {
+      async: true,
+      nopipe: true,
+    });
+  }
+  startApp();
+};
+
+(() => {
+  preview();
+})();

+ 18 - 0
build/tsconfig.json

@@ -0,0 +1,18 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "commonjs",
+    "moduleResolution": "node",
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "jsx": "react",
+    "baseUrl": ".",
+    "esModuleInterop": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "experimentalDecorators": true,
+    "lib": ["dom", "esnext"],
+    "incremental": true,
+    "skipLibCheck": true
+  }
+}

+ 87 - 0
build/utils.ts

@@ -0,0 +1,87 @@
+import fs from 'fs';
+import { networkInterfaces } from 'os';
+import dotenv from 'dotenv';
+
+export const isFunction = (arg: unknown): arg is (...args: any[]) => any =>
+  typeof arg === 'function';
+export const isRegExp = (arg: unknown): arg is RegExp =>
+  Object.prototype.toString.call(arg) === '[object RegExp]';
+
+/*
+ * Read all files in the specified folder, filter through regular rules, and return file path array
+ * @param root Specify the folder path
+ * [@param] reg Regular expression for filtering files, optional parameters
+ * Note: It can also be deformed to check whether the file path conforms to regular rules. The path can be a folder or a file. The path that does not exist is also fault-tolerant.
+ */
+export function readAllFile(root: string, reg: RegExp) {
+  let resultArr: string[] = [];
+  try {
+    if (fs.existsSync(root)) {
+      const stat = fs.lstatSync(root);
+      if (stat.isDirectory()) {
+        // dir
+        const files = fs.readdirSync(root);
+        files.forEach(function (file) {
+          const t = readAllFile(root + '/' + file, reg);
+          resultArr = resultArr.concat(t);
+        });
+      } else {
+        if (reg !== undefined) {
+          if (isFunction(reg.test) && reg.test(root)) {
+            resultArr.push(root);
+          }
+        } else {
+          resultArr.push(root);
+        }
+      }
+    }
+  } catch (error) {}
+
+  return resultArr;
+}
+
+export function getIPAddress() {
+  let interfaces = networkInterfaces();
+  for (let devName in interfaces) {
+    let iFace = interfaces[devName];
+    if (!iFace) return;
+    for (let i = 0; i < iFace.length; i++) {
+      let alias = iFace[i];
+      if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
+        return alias.address;
+      }
+    }
+  }
+
+  return '';
+}
+
+export function isDevFn(): boolean {
+  return process.env.NODE_ENV === 'development';
+}
+
+export function isProdFn(): boolean {
+  return process.env.NODE_ENV === 'production';
+}
+
+export function isReportMode(): boolean {
+  return process.env.REPORT === 'true';
+}
+
+export function loadEnv() {
+  const env = process.env.NODE_ENV;
+  const ret: any = {};
+  const envList = [`.env.${env}.local`, `.env.${env}`, '.env.local', '.env', ,];
+  envList.forEach((e) => {
+    dotenv.config({
+      path: e,
+    });
+  });
+
+  for (const envName of Object.keys(process.env)) {
+    const realName = (process.env as any)[envName].replace(/\\n/g, '\n');
+    ret[envName] = realName;
+    process.env[envName] = realName;
+  }
+  return ret;
+}

+ 55 - 0
commitlint.config.js

@@ -0,0 +1,55 @@
+module.exports = {
+  ignores: [(commit) => commit.includes('init')],
+  extends: ['@commitlint/config-conventional'],
+  parserPreset: {
+    parserOpts: {
+      headerPattern: /^(\w*|[\u4e00-\u9fa5]*)(?:[\(\(](.*)[\)\)])?[\:\:] (.*)/,
+      headerCorrespondence: ['type', 'scope', 'subject'],
+      referenceActions: [
+        'close',
+        'closes',
+        'closed',
+        'fix',
+        'fixes',
+        'fixed',
+        'resolve',
+        'resolves',
+        'resolved',
+      ],
+      issuePrefixes: ['#'],
+      noteKeywords: ['BREAKING CHANGE', '不兼容变更'],
+      fieldPattern: /^-(.*?)-$/,
+      revertPattern: /^Revert\s"([\s\S]*)"\s*This reverts commit (\w*)\./,
+      revertCorrespondence: ['header', 'hash'],
+      warn() {},
+      mergePattern: null,
+      mergeCorrespondence: null,
+    },
+  },
+  rules: {
+    'body-leading-blank': [2, 'always'],
+    'footer-leading-blank': [1, 'always'],
+    'header-max-length': [2, 'always', 108],
+    'subject-empty': [2, 'never'],
+    'type-empty': [2, 'never'],
+    'type-enum': [
+      2,
+      'always',
+      [
+        'feat',
+        'fix',
+        'perf',
+        'style',
+        'docs',
+        'test',
+        'refactor',
+        'build',
+        'ci',
+        'chore',
+        'revert',
+        'wip',
+        'workflow',
+      ],
+    ],
+  },
+};

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vue Vben admin 2.0</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 8 - 0
lint-staged.config.js

@@ -0,0 +1,8 @@
+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': ['prettier --write', 'stylelint --fix', 'git add .'],
+  '*.{scss,less,styl,css,html}': ['stylelint --fix', 'prettier --write', 'git add .'],
+  '*.md': ['prettier --write'],
+};

+ 7 - 0
mock/_createProductionServer.ts

@@ -0,0 +1,7 @@
+import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
+import userMock from './sys/user';
+import menuMock from './sys/menu';
+
+export function setupProdMockServer() {
+  createProdMockServer([...userMock, ...menuMock]);
+}

+ 38 - 0
mock/_util.ts

@@ -0,0 +1,38 @@
+// Interface data format used to return a unified format
+
+export function resultSuccess<T = any>(result: T, { message = 'ok' } = {}) {
+  return {
+    code: 0,
+    result,
+    message,
+    type: 'success',
+  };
+}
+
+export function resultPageSuccess<T = any>(items: T[], total: number, { message = 'ok' } = {}) {
+  return {
+    code: 0,
+    result: {
+      items,
+      total,
+    },
+    message,
+    type: 'success',
+  };
+}
+
+export function resultError(message = 'Request failed', { code = -1, result = null } = {}) {
+  return {
+    code,
+    result,
+    message,
+    type: 'error',
+  };
+}
+
+export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]): T[] {
+  let offset = (pageNo - 1) * pageSize;
+  return offset + pageSize >= array.length
+    ? array.slice(offset, array.length)
+    : array.slice(offset, offset + pageSize);
+}

+ 132 - 0
mock/sys/menu.ts

@@ -0,0 +1,132 @@
+import { resultSuccess } from '../_util';
+import { MockMethod } from 'vite-plugin-mock';
+
+const dashboardRoute = {
+  layout: {
+    path: '/dashboard',
+    name: 'Dashboard',
+    component: 'PAGE_LAYOUT',
+    redirect: '/dashboard/welcome',
+    meta: {
+      icon: 'ant-design:home-outlined',
+      title: 'Dashboard',
+    },
+  },
+  routes: [
+    {
+      path: '/welcome',
+      name: 'Welcome',
+      component: '/dashboard/welcome/index.vue',
+      meta: {
+        title: '欢迎页',
+        affix: true,
+      },
+    },
+  ],
+};
+
+const frontRoute = {
+  path: '/front',
+  name: 'PermissionFrontDemo',
+  meta: {
+    title: '基于前端权限',
+  },
+  children: [
+    {
+      path: 'page',
+      component: '/demo/permission/front/index.vue',
+      meta: {
+        title: '页面权限',
+      },
+    },
+    {
+      path: 'btn',
+      component: '/demo/permission/front/Btn.vue',
+      meta: {
+        title: '按钮权限',
+      },
+    },
+    {
+      path: 'auth-pageA',
+      component: '/demo/permission/front/AuthPageA.vue',
+      meta: {
+        title: '权限测试页A',
+      },
+    },
+    {
+      path: 'auth-pageB',
+      component: '/demo/permission/front/AuthPageB.vue',
+      meta: {
+        title: '权限测试页B',
+      },
+    },
+  ],
+};
+const backRoute = {
+  path: '/back',
+  name: 'PermissionBackDemo',
+  meta: {
+    title: '基于后台权限',
+  },
+  children: [
+    {
+      path: 'page',
+      component: 'demo/permission/back/index.vue',
+      meta: {
+        title: '页面权限',
+      },
+    },
+    {
+      path: 'btn',
+      component: '/demo/permission/back/Btn.vue',
+      meta: {
+        title: '按钮权限',
+      },
+    },
+  ],
+};
+const authRoute = {
+  layout: {
+    path: '/permission',
+    name: 'Permission',
+    component: 'PAGE_LAYOUT',
+    redirect: '/permission/front/page',
+    meta: {
+      icon: 'ant-design:home-outlined',
+      title: '权限管理',
+    },
+  },
+
+  routes: [frontRoute, backRoute],
+};
+
+const authRoute1 = {
+  layout: {
+    path: '/permission',
+    name: 'Permission',
+    component: 'PAGE_LAYOUT',
+    redirect: '/permission/front/page',
+    meta: {
+      icon: 'ant-design:home-outlined',
+      title: '权限管理',
+    },
+  },
+
+  routes: [backRoute],
+};
+export default [
+  {
+    url: '/api/getMenuListById',
+    timeout: 1000,
+    method: 'get',
+    response: ({ query }) => {
+      const { id } = query;
+      if (!id || id === '1') {
+        return resultSuccess([dashboardRoute, authRoute]);
+      }
+      if (id === '2') {
+        return resultSuccess([dashboardRoute, authRoute1]);
+      }
+    },
+  },
+] as MockMethod[];

+ 90 - 0
mock/sys/user.ts

@@ -0,0 +1,90 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { resultError, resultSuccess } from '../_util';
+
+function createFakeUserList() {
+  return [
+    {
+      userId: '1',
+      username: 'vben',
+      realName: 'Vben',
+      desc: 'manager',
+      password: '123456',
+      token: 'fakeToken1',
+      role: {
+        roleName: 'Super Admin',
+        value: 'super',
+      },
+    },
+    {
+      userId: '2',
+      username: 'test',
+      password: '123456',
+      realName: 'test user',
+      desc: 'tester',
+      token: 'fakeToken2',
+      role: {
+        roleName: 'Tester',
+        value: 'test',
+      },
+    },
+  ];
+}
+
+const fakeCodeList: any = {
+  '1': ['1000', '3000', '5000'],
+
+  '2': ['2000', '4000', '6000'],
+};
+export default [
+  // mock user login
+  {
+    url: '/api/login',
+    timeout: 1000,
+    method: 'post',
+    response: ({ body }) => {
+      const { username, password } = body;
+      const checkUser = createFakeUserList().find(
+        (item) => item.username === username && password === item.password
+      );
+      if (!checkUser) {
+        return resultError('Incorrect account or password!');
+      }
+      const { userId, username: _username, token, realName, desc, role } = checkUser;
+      return resultSuccess({
+        role,
+        userId,
+        username: _username,
+        token,
+        realName,
+        desc,
+      });
+    },
+  },
+  {
+    url: '/api/getUserInfoById',
+    timeout: 200,
+    method: 'get',
+    response: ({ query }) => {
+      const { userId } = query;
+      const checkUser = createFakeUserList().find((item) => item.userId === userId);
+      if (!checkUser) {
+        return resultError('The corresponding user information was not obtained!');
+      }
+      return resultSuccess(checkUser);
+    },
+  },
+  {
+    url: '/api/getPermCodeByUserId',
+    timeout: 200,
+    method: 'get',
+    response: ({ query }) => {
+      const { userId } = query;
+      if (!userId) {
+        return resultError('userId is not null!');
+      }
+      const codeList = fakeCodeList[userId];
+
+      return resultSuccess(codeList);
+    },
+  },
+] as MockMethod[];

+ 104 - 0
package.json

@@ -0,0 +1,104 @@
+{
+  "name": "vben-admin-2.0",
+  "version": "2.0.0-beta.1",
+  "scripts": {
+    "bootstrap": "yarn install",
+    "serve": "ts-node --project ./build/tsconfig.json  ./build/script/preserve && cross-env NODE_ENV=development vite",
+    "build": "cross-env NODE_ENV=production vite build ",
+    "report": "cross-env REPORT=true yarn build ",
+    "build:no-cache": "yarn  clean:cache && yarn build",
+    "preview": "ts-node --project ./build/tsconfig.json  ./build/script/preview",
+    "log": "ts-node --project ./build/tsconfig.json  ./build/script/changelog",
+    "gen:gz": "ts-node --project build/tsconfig.build.json ./build/gzip/index.ts ",
+    "clean:cache": "npx rimraf node_modules/.cache/ && npx rimraf node_modules/.vite_opt_cache",
+    "clean:lib": "npx rimraf node_modules",
+    "ls-lint": "npx ls-lint",
+    "lint:eslint": "eslint --fix --ext \"src/**/*.{vue,less,css,scss}\"",
+    "lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
+    "lint:stylelint": "stylelint  --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
+    "reinstall": "npx rimraf node_modules && npx rimraf yarn.lock && npx rimraf package.lock.json && yarn run bootstrap",
+    "postinstall": "ts-node --project ./build/tsconfig.json  ./build/script/postinstall"
+  },
+  "dependencies": {
+    "@iconify/iconify": "^2.0.0-rc.1",
+    "ant-design-vue": "^2.0.0-beta.10",
+    "axios": "^0.20.0",
+    "lodash-es": "^4.17.15",
+    "mockjs": "^1.1.0",
+    "nprogress": "^0.2.0",
+    "path-to-regexp": "^6.1.0",
+    "qrcode": "^1.4.4",
+    "vue": "^3.0.0",
+    "vue-i18n": "^9.0.0-beta.3",
+    "vue-router": "^4.0.0-beta.12",
+    "vuex": "^4.0.0-beta.4",
+    "vuex-module-decorators": "^1.0.1",
+    "zxcvbn": "^4.4.2"
+  },
+  "devDependencies": {
+    "@commitlint/cli": "^11.0.0",
+    "@commitlint/config-conventional": "^11.0.0",
+    "@iconify/json": "^1.1.233",
+    "@ls-lint/ls-lint": "^1.9.2",
+    "@purge-icons/generated": "^0.4.1",
+    "@types/fs-extra": "^9.0.1",
+    "@types/inquirer": "^7.3.1",
+    "@types/koa-static": "^4.0.1",
+    "@types/lodash-es": "^4.17.3",
+    "@types/mockjs": "^1.0.3",
+    "@types/nprogress": "^0.2.0",
+    "@types/qrcode": "^1.3.5",
+    "@types/rollup-plugin-visualizer": "^2.6.0",
+    "@types/shelljs": "^0.8.8",
+    "@types/zxcvbn": "^4.4.0",
+    "@typescript-eslint/eslint-plugin": "^4.2.0",
+    "@typescript-eslint/parser": "^4.2.0",
+    "@vue/compiler-sfc": "^3.0.0",
+    "autoprefixer": "^9.8.6",
+    "babel-plugin-import": "^1.13.0",
+    "commitizen": "^4.2.1",
+    "conventional-changelog-cli": "^2.1.0",
+    "cross-env": "^7.0.2",
+    "dotenv": "^8.2.0",
+    "eslint": "^7.10.0",
+    "eslint-config-prettier": "^6.12.0",
+    "eslint-plugin-prettier": "^3.1.4",
+    "eslint-plugin-vue": "^7.0.0-beta.4",
+    "fs-extra": "^9.0.1",
+    "husky": "^4.3.0",
+    "inquirer": "^7.3.3",
+    "koa-static": "^5.0.0",
+    "less": "^3.12.2",
+    "lint-staged": "^10.4.0",
+    "ora": "^5.1.0",
+    "portfinder": "^1.0.28",
+    "postcss-import": "^12.0.1",
+    "prettier": "^2.1.2",
+    "rimraf": "^3.0.2",
+    "rollup-plugin-analyzer": "^3.3.0",
+    "rollup-plugin-visualizer": "^4.1.1",
+    "shelljs": "^0.8.4",
+    "stylelint": "^13.7.2",
+    "stylelint-config-prettier": "^8.0.2",
+    "stylelint-config-standard": "^20.0.0",
+    "stylelint-order": "^4.1.0",
+    "tailwindcss": "^1.8.10",
+    "tasksfile": "^5.1.1",
+    "ts-node": "^9.0.0",
+    "typescript": "^4.0.3",
+    "vite": "^1.0.0-rc.4",
+    "vite-jsx": "^1.0.5",
+    "vite-plugin-mock": "^1.0.2",
+    "vite-plugin-purge-icons": "^0.4.1",
+    "vue-eslint-parser": "^7.1.0"
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "ls-lint && lint-staged",
+      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
+    }
+  },
+  "engines": {
+    "node": ">=12.0.0"
+  }
+}

+ 4 - 0
postcss.config.js

@@ -0,0 +1,4 @@
+const path = require('path');
+module.exports = {
+  plugins: [require('tailwindcss'), require('autoprefixer'), require('postcss-import')],
+};

+ 28 - 0
prettier.config.js

@@ -0,0 +1,28 @@
+module.exports = {
+  printWidth: 100,
+  tabWidth: 2,
+  useTabs: false,
+  semi: true,
+  vueIndentScriptAndStyle: true,
+  singleQuote: true,
+  quoteProps: 'as-needed',
+  bracketSpacing: true,
+  trailingComma: 'es5',
+  jsxBracketSameLine: false,
+  jsxSingleQuote: false,
+  arrowParens: 'always',
+  insertPragma: false,
+  requirePragma: false,
+  proseWrap: 'never',
+  htmlWhitespaceSensitivity: 'strict',
+  endOfLine: 'lf',
+  rangeStart: 0,
+  overrides: [
+    {
+      files: '*.md',
+      options: {
+        tabWidth: 2,
+      },
+    },
+  ],
+};

BIN
public/favicon.ico


+ 42 - 0
src/App.vue

@@ -0,0 +1,42 @@
+<template>
+  <ConfigProvider
+    :locale="zhCN"
+    :renderEmpty="renderEmpty"
+    :transformCellText="transformCellText"
+    v-bind="lockOn"
+  >
+    <router-view />
+  </ConfigProvider>
+</template>
+
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { ConfigProvider } from 'ant-design-vue';
+  import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
+
+  import zhCN from 'ant-design-vue/es/locale/zh_CN';
+  import moment from 'moment';
+  import 'moment/locale/zh-cn';
+
+  import { useConfigProvider, useInitAppConfigStore, useListenerNetWork } from './useApp';
+  import { useLockPage } from '/@/hooks/web/useLockPage';
+  moment.locale('zh-cn');
+  export default defineComponent({
+    name: 'App',
+    components: { ConfigProvider },
+    setup() {
+      useInitAppConfigStore();
+      useListenerNetWork();
+      createBreakpointListen();
+      const { renderEmpty, transformCellText } = useConfigProvider();
+      const { on: lockOn } = useLockPage();
+
+      return {
+        renderEmpty,
+        transformCellText,
+        zhCN,
+        lockOn,
+      };
+    },
+  });
+</script>

+ 18 - 0
src/api/sys/menu.ts

@@ -0,0 +1,18 @@
+import { defHttp } from '/@/utils/http/axios';
+
+import { getMenuListByIdParams, getMenuListByIdParamsResultModel } from './model/menuModel';
+
+enum Api {
+  GetMenuListById = '/getMenuListById',
+}
+
+/**
+ * @description: 根据id获取用户菜单
+ */
+export function getMenuListById(params: getMenuListByIdParams) {
+  return defHttp.request<getMenuListByIdParamsResultModel>({
+    url: Api.GetMenuListById,
+    method: 'GET',
+    params,
+  });
+}

+ 23 - 0
src/api/sys/model/menuModel.ts

@@ -0,0 +1,23 @@
+import { RouteMeta } from '/@/router/types';
+export interface RouteItem {
+  path: string;
+  component: any;
+  meta: RouteMeta;
+  name?: string;
+  alias?: string | string[];
+  redirect?: string;
+  caseSensitive?: boolean;
+  children?: RouteItem[];
+}
+
+/**
+ * @description: 获取菜单接口
+ */
+export interface getMenuListByIdParams {
+  id: number | string;
+}
+
+/**
+ * @description: 获取菜单返回值
+ */
+export type getMenuListByIdParamsResultModel = RouteItem[];

+ 43 - 0
src/api/sys/model/userModel.ts

@@ -0,0 +1,43 @@
+/**
+ * @description: Login interface parameters
+ */
+export interface LoginParams {
+  username: string;
+  password: string;
+}
+
+/**
+ * @description: Get user information
+ */
+export interface GetUserInfoByUserIdParams {
+  userId: string | number;
+}
+
+export interface RoleInfo {
+  roleName: string;
+  value: string;
+}
+
+/**
+ * @description: Login interface return value
+ */
+export interface LoginResultModel {
+  userId: string | number;
+  token: string;
+  role: RoleInfo;
+}
+
+/**
+ * @description: Get user information return value
+ */
+export interface GetUserInfoByUserIdModel {
+  role: RoleInfo;
+  // 用户id
+  userId: string | number;
+  // 用户名
+  username: string;
+  // 真实名字
+  realName: string;
+  // 介绍
+  desc?: string;
+}

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

@@ -0,0 +1,48 @@
+import { defHttp } from '/@/utils/http/axios';
+import {
+  LoginParams,
+  LoginResultModel,
+  GetUserInfoByUserIdParams,
+  GetUserInfoByUserIdModel,
+} from './model/userModel';
+
+enum Api {
+  Login = '/login',
+  GetUserInfoById = '/getUserInfoById',
+  GetPermCodeByUserId = '/getPermCodeByUserId',
+}
+
+/**
+ * @description: user login api
+ */
+export function loginApi(params: LoginParams) {
+  return defHttp.request<LoginResultModel>(
+    {
+      url: Api.Login,
+      method: 'POST',
+      params,
+    },
+    {
+      errorMessageMode: 'modal',
+    }
+  );
+}
+
+/**
+ * @description: getUserInfoById
+ */
+export function getUserInfoById(params: GetUserInfoByUserIdParams) {
+  return defHttp.request<GetUserInfoByUserIdModel>({
+    url: Api.GetUserInfoById,
+    method: 'GET',
+    params,
+  });
+}
+
+export function getPermCodeByUserId(params: GetUserInfoByUserIdParams) {
+  return defHttp.request<string[]>({
+    url: Api.GetPermCodeByUserId,
+    method: 'GET',
+    params,
+  });
+}

BIN
src/assets/images/dashboard/wokb/approve.png


BIN
src/assets/images/dashboard/wokb/attendance.png


BIN
src/assets/images/dashboard/wokb/datashow1.png


BIN
src/assets/images/dashboard/wokb/datashow2.png


BIN
src/assets/images/dashboard/wokb/datashow3.png


BIN
src/assets/images/dashboard/wokb/datashow4.png


BIN
src/assets/images/dashboard/wokb/leave.png


BIN
src/assets/images/dashboard/wokb/meal.png


BIN
src/assets/images/dashboard/wokb/overtime.png


BIN
src/assets/images/dashboard/wokb/performance.png


BIN
src/assets/images/dashboard/wokb/stamp.png


BIN
src/assets/images/dashboard/wokb/travel.png


BIN
src/assets/images/dashboard/wokb/wokb.png


BIN
src/assets/images/exception/404.png


BIN
src/assets/images/exception/500.png


BIN
src/assets/images/exception/net-work.png


BIN
src/assets/images/header.jpg


+ 39 - 0
src/assets/images/layout/menu-mix.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1" 
+    xmlns="http://www.w3.org/2000/svg" 
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+                <feMergeNode in="SourceGraphic"></feMergeNode>
+            </feMerge>
+        </filter>
+        <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+        <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="配置面板" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
+            <g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
+                <g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
+                    <mask id="mask-3" fill="white">
+                        <use xlink:href="#path-2"></use>
+                    </mask>
+                    <g id="Rectangle-18">
+                        <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+                        <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+                    </g>
+                    <rect id="Rectangle-18" fill="#fff" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+                    <rect id="Rectangle-11" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 39 - 0
src/assets/images/layout/menu-sidebar.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
+  xmlns="http://www.w3.org/2000/svg"
+  xmlns:xlink="http://www.w3.org/1999/xlink">
+  <defs>
+    <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+      <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+      <feMerge>
+        <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+        <feMergeNode in="SourceGraphic"></feMergeNode>
+      </feMerge>
+    </filter>
+    <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+    <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+      <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+    </filter>
+  </defs>
+  <g width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
+      <g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
+        <g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
+          <mask id="mask-3" fill="white">
+            <use xlink:href="#path-2"></use>
+          </mask>
+          <g id="Rectangle-18">
+            <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+            <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+          </g>
+          <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+          <rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 39 - 0
src/assets/images/layout/menu-top.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
+  xmlns="http://www.w3.org/2000/svg"
+  xmlns:xlink="http://www.w3.org/1999/xlink">
+
+  <defs>
+    <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+      <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+      <feMerge>
+        <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+        <feMergeNode in="SourceGraphic"></feMergeNode>
+      </feMerge>
+    </filter>
+    <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+    <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+      <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+    </filter>
+  </defs>
+  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="setting-copy-2" transform="translate(-1254.000000, -337.000000)">
+      <g id="Group-8" transform="translate(1167.000000, 0.000000)">
+        <g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 338.000000)">
+          <mask id="mask-3" fill="white">
+            <use xlink:href="#path-2"></use>
+          </mask>
+          <g id="Rectangle-18">
+            <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+            <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+          </g>
+          <rect id="Rectangle-11" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 67 - 0
src/assets/images/loading.svg

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg viewBox="0 0 200 200" version="1.1" 
+  xmlns="http://www.w3.org/2000/svg" 
+  xmlns:xlink="http://www.w3.org/1999/xlink">
+  <style type="text/css">
+        .left-linear {
+          fill: url(#left-linear);
+        }
+
+        .right-linear {
+          fill: url(#right-linear);
+        }
+
+        .top {
+          fill: #64acff;
+        }
+
+        .bottom {
+          fill: #9dbfe4;
+        }
+        @keyframes load {
+          0% {
+            transform: rotate(-360deg);
+          }
+
+          100% {
+            transform: rotate(0);
+          }
+        }
+
+        .load {
+          animation: load 1.4s linear infinite;
+          transform-origin: center center;
+        }
+
+        svg {
+          display: block;
+        }
+
+        .tip {
+          display: block;
+          min-width: 100px;
+          margin-top: 4px;
+          font-size: 13px;
+          color: #303133;
+          text-align: left;
+        }
+  </style>
+  <circle cx="97" cy="97" r="81" stroke-width="16" stroke="#327fd8" fill="none"></circle>
+  <g class="load">
+    <!--右半圆环-->
+    <linearGradient id="left-linear" gradientUnits="userSpaceOnUse" x1="50" y1="0" x2="100" y2="180">
+      <stop offset="0" style="stop-color: #64acff;" />
+      <stop offset="1" style="stop-color: #9DBFE4;" />
+    </linearGradient>
+    <path class="left-linear" d="M20,100c0-44.1,35.9-80,80-80V0C44.8,0,0,44.8,0,100s44.8,100,100,100v-20C55.9,180,20,144.1,20,100z" />
+    <!--左半圆环-->
+    <circle class="bottom" cx="100" cy="190" r="10" />
+    <linearGradient id="right-linear" gradientUnits="userSpaceOnUse" x1="100" y1="120" x2="100" y2="180">
+      <stop offset="0" style="stop-color: transparent;" />
+      <stop offset="1" style="stop-color: transparent;" />
+    </linearGradient>
+    <path class="right-linear" d="M100,0v20c44.1,0,80,35.9,80,80c0,44.1-35.9,80-80,80v20c55.2,0,100-44.8,100-100S155.2,0,100,0z" />
+    <!--左半圆环-->
+    <circle class="top" cx="100" cy="10" r="10" />
+  </g>
+</svg>

BIN
src/assets/images/lock-page.jpg


BIN
src/assets/images/lock-page.png


BIN
src/assets/images/login/login-bg.png


BIN
src/assets/images/login/login-in.png


BIN
src/assets/images/logo.png


BIN
src/assets/images/no-data.png


BIN
src/assets/images/page_null.png


BIN
src/assets/images/qq.jpeg


BIN
src/assets/images/sidebar/dark-mini.png


BIN
src/assets/images/sidebar/dark.png


BIN
src/assets/images/sidebar/light-mini.png


BIN
src/assets/images/sidebar/light.png


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
src/assets/svg/preview/p-rotate.svg


+ 1 - 0
src/assets/svg/preview/resume.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595307154239" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7317" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M316 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8zM512 622c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39zM512 482c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39z" p-id="7318" fill="#ffffff"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z" p-id="7319" fill="#ffffff"></path><path d="M648 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8z" p-id="7320" fill="#ffffff"></path></svg>

+ 1 - 0
src/assets/svg/preview/scale.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595307195033" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8116" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M887.081 904.791a25.8 25.8 0 0 1-18.376-7.619L705.618 734.075l-4.163 3.369c-58.255 47.18-131.522 73.16-206.32 73.16-181.07 0-328.377-147.308-328.377-328.367 0-181.068 147.308-328.376 328.377-328.376 181.063 0 328.376 147.308 328.376 328.376 0 77.072-27.412 152.07-77.169 211.17l-3.522 4.173 162.719 162.744a25.846 25.846 0 0 1 7.639 18.432 26.081 26.081 0 0 1-26.051 26.045l-0.046-0.01zM495.13 205.957c-152.336 0-276.27 123.935-276.27 276.27 0 152.33 123.934 276.27 276.27 276.27 152.34 0 276.275-123.94 276.275-276.27 0-152.335-123.935-276.27-276.275-276.27z" fill="#ffffff" p-id="8117"></path><path d="M626.545 508.355h-262.83a26.127 26.127 0 0 1 0-52.255h262.83a26.127 26.127 0 0 1 0 52.255z" fill="#ffffff" p-id="8118"></path><path d="M495.13 639.77a26.127 26.127 0 0 1-26.128-26.128v-262.83a26.127 26.127 0 0 1 52.255 0v262.835a26.127 26.127 0 0 1-26.127 26.123z" fill="#ffffff" p-id="8119"></path></svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
src/assets/svg/preview/unrotate.svg


+ 1 - 0
src/assets/svg/preview/unscale.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595308005241" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9878" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M750.3 198.7C598 46.4 351.1 46.4 198.7 198.7s-152.3 399.2 0 551.5C345.1 896.6 578.8 902.3 732 767.3l172.1 172.1 35.4-35.4-172.1-171.9c135-153.2 129.3-387-17.1-533.4z m39.3 403.8c-17.1 42.1-42.2 80-74.7 112.4-32.5 32.5-70.3 57.6-112.4 74.7-40.7 16.5-83.8 24.9-128 24.9s-87.2-8.4-128-24.9c-42.1-17.1-80-42.2-112.4-74.7s-57.6-70.3-74.7-112.4c-16.5-40.7-24.9-83.8-24.9-128s8.4-87.2 24.9-128c17.1-42.1 42.2-80 74.7-112.4s70.3-57.6 112.4-74.7c40.7-16.5 83.8-24.9 128-24.9s87.2 8.4 128 24.9c42.1 17.1 80 42.2 112.4 74.7 32.5 32.5 57.6 70.3 74.7 112.4 16.5 40.7 24.9 83.8 24.9 128s-8.4 87.3-24.9 128zM671 502H271v-50h400v50z" fill="#ffffff" p-id="9879"></path></svg>

+ 59 - 0
src/components/Authority/index.tsx

@@ -0,0 +1,59 @@
+import { defineComponent, PropType, computed, unref } from 'vue';
+
+import { PermissionModeEnum } from '/@/enums/appEnum';
+import { RoleEnum } from '/@/enums/roleEnum';
+import { usePermission } from '/@/hooks/web/usePermission';
+import { appStore } from '/@/store/modules/app';
+import { getSlot } from '/@/utils/helper/tsxHelper';
+
+export default defineComponent({
+  name: 'Authority',
+  props: {
+    // 指定角色可见
+    value: {
+      type: [Number, Array, String] as PropType<RoleEnum | RoleEnum[]>,
+      default: '',
+    },
+  },
+  setup(props, { slots }) {
+    const getModeRef = computed(() => {
+      return appStore.getProjectConfig.permissionMode;
+    });
+    /**
+     * 渲染角色按钮
+     */
+    function renderRoleAuth() {
+      const { value } = props;
+      if (!value) {
+        return getSlot(slots, 'default');
+      }
+      const { hasPermission } = usePermission();
+      return hasPermission(value) ? getSlot(slots, 'default') : null;
+    }
+
+    /**
+     * 渲染编码按钮
+     * 这里只判断是否包含,具体实现可以根据项目自行写逻辑
+     */
+    function renderCodeAuth() {
+      const { value } = props;
+      if (!value) {
+        return getSlot(slots, 'default');
+      }
+      const { hasPermission } = usePermission();
+      return hasPermission(value) ? getSlot(slots, 'default') : null;
+    }
+    return () => {
+      const mode = unref(getModeRef);
+      // 基于角色渲染
+      if (mode === PermissionModeEnum.ROLE) {
+        return renderRoleAuth();
+      }
+      // 基于后台编码渲染
+      if (mode === PermissionModeEnum.BACK) {
+        return renderCodeAuth();
+      }
+      return getSlot(slots, 'default');
+    };
+  },
+});

+ 4 - 0
src/components/Basic/index.ts

@@ -0,0 +1,4 @@
+export { default as BasicArrow } from './src/BasicArrow.vue';
+export { default as BasicHelp } from './src/BasicHelp';
+export { default as BasicTitle } from './src/BasicTitle.vue';
+export { default as BasicEmpty } from './src/BasicEmpty.vue';

+ 53 - 0
src/components/Basic/src/BasicArrow.vue

@@ -0,0 +1,53 @@
+<template>
+  <span :class="getClass">
+    <RightOutlined />
+  </span>
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+
+  import { defineComponent, computed } from 'vue';
+
+  import { RightOutlined } from '@ant-design/icons-vue';
+
+  export default defineComponent({
+    name: 'BaseArrow',
+    components: { RightOutlined },
+    props: {
+      // Expand contract, expand by default
+      expand: {
+        type: Boolean as PropType<boolean>,
+        default: true,
+      },
+    },
+    setup(props) {
+      const getClass = computed(() => {
+        const preCls = 'base-arrow';
+        const cls = [preCls];
+
+        props.expand && cls.push(`${preCls}__active`);
+        return cls;
+      });
+
+      return {
+        getClass,
+      };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .base-arrow {
+    transform: rotate(-90deg) !important;
+    transition: all 0.3s ease 0.1s;
+    transform-origin: center center;
+
+    &.right {
+      transform: rotate(0deg);
+    }
+
+    &__active {
+      transform: rotate(90deg) !important;
+      transition: all 0.3s ease 0.1s !important;
+    }
+  }
+</style>

+ 28 - 0
src/components/Basic/src/BasicEmpty.vue

@@ -0,0 +1,28 @@
+<template>
+  <Empty :image="image" :description="description" />
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { Empty } from 'ant-design-vue';
+
+  import emptySrc from '/@/assets/images/page_null.png';
+
+  export default defineComponent({
+    extends: Empty as any,
+    components: { Empty },
+    props: {
+      description: {
+        type: String,
+        default: '暂无内容',
+      },
+      image: {
+        type: String,
+        default: emptySrc,
+        required: false,
+      },
+    },
+    setup() {
+      return {};
+    },
+  });
+</script>

+ 18 - 0
src/components/Basic/src/BasicHelp.less

@@ -0,0 +1,18 @@
+@import (reference) '../../../design/index.less';
+
+.base-help {
+  display: inline-block;
+  font-size: 14px;
+  color: @text-color-help-dark;
+  cursor: pointer;
+
+  &:hover {
+    color: @primary-color;
+  }
+
+  &__wrap {
+    p {
+      margin-bottom: 0;
+    }
+  }
+}

+ 107 - 0
src/components/Basic/src/BasicHelp.tsx

@@ -0,0 +1,107 @@
+import type { PropType } from 'vue';
+
+import { Tooltip } from 'ant-design-vue';
+import { InfoCircleOutlined } from '@ant-design/icons-vue';
+import { defineComponent, computed, unref } from 'vue';
+
+import { getPopupContainer } from '/@/utils';
+
+import { isString, isArray } from '/@/utils/is';
+import { getSlot } from '/@/utils/helper/tsxHelper';
+import './BasicHelp.less';
+export default defineComponent({
+  name: 'BaseHelp',
+  props: {
+    // max-width
+    maxWidth: {
+      type: String as PropType<string>,
+      default: '600px',
+    },
+    // Whether to display the serial number
+    showIndex: {
+      type: Boolean as PropType<boolean>,
+      default: false,
+    },
+    // Text list
+    text: {
+      type: [Array, String] as PropType<string[] | string>,
+    },
+    // color
+    color: {
+      type: String as PropType<string>,
+      default: '#ffffff',
+    },
+    fontSize: {
+      type: String as PropType<string>,
+      default: '14px',
+    },
+    absolute: {
+      type: Boolean as PropType<boolean>,
+      default: false,
+    },
+    // 定位
+    position: {
+      type: [Object] as PropType<any>,
+      default: () => ({
+        position: 'absolute',
+        left: 0,
+        bottom: 0,
+      }),
+    },
+  },
+  setup(props, { slots }) {
+    const getOverlayStyleRef = computed(() => {
+      return {
+        maxWidth: props.maxWidth,
+      };
+    });
+    const getWrapStyleRef = computed(() => {
+      return {
+        color: props.color,
+        fontSize: props.fontSize,
+      };
+    });
+    const getMainStyleRef = computed(() => {
+      return props.absolute ? props.position : {};
+    });
+
+    /**
+     * @description: 渲染内容
+     */
+    const renderTitle = () => {
+      const list = props.text;
+      if (isString(list)) {
+        return <p>{list}</p>;
+      }
+      if (isArray(list)) {
+        return list.map((item, index) => {
+          return (
+            <p key={item}>
+              {props.showIndex ? `${index + 1}. ` : ''}
+              {item}
+            </p>
+          );
+        });
+      }
+      return null;
+    };
+    return () => (
+      <Tooltip
+        title={(<div style={unref(getWrapStyleRef)}>{renderTitle()}</div>) as any}
+        placement="right"
+        overlayStyle={unref(getOverlayStyleRef)}
+        autoAdjustOverflow={true}
+        overlayClassName="base-help__wrap"
+        getPopupContainer={() => getPopupContainer()}
+      >
+        {{
+          default: () => (
+            <span class="base-help" style={unref(getMainStyleRef)}>
+              {getSlot(slots) || <InfoCircleOutlined />}
+            </span>
+          ),
+        }}
+      </Tooltip>
+    );
+  },
+});

+ 58 - 0
src/components/Basic/src/BasicTitle.vue

@@ -0,0 +1,58 @@
+<template>
+  <span class="base-title" :class="{ 'show-span': showSpan && $slots.default }">
+    <slot />
+    <BaseHelp class="base-title__help" v-if="helpMessage" :text="helpMessage" />
+  </span>
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+
+  import { defineComponent } from 'vue';
+
+  export default defineComponent({
+    name: 'BaseTitle',
+    props: {
+      helpMessage: {
+        type: [String, Array] as PropType<string | string[]>,
+        default: '',
+      },
+      showSpan: {
+        type: Boolean as PropType<boolean>,
+        default: true,
+      },
+    },
+    setup() {
+      return {};
+    },
+  });
+</script>
+<style lang="less" scoped>
+  @import (reference) '../../../design/index.less';
+
+  .base-title {
+    position: relative;
+    display: flex;
+    padding-left: 7px;
+    font-size: 16px;
+    font-weight: 700;
+    line-height: 24px;
+    color: @text-color-base;
+
+    .unselect();
+
+    &.show-span::before {
+      position: absolute;
+      top: 4px;
+      left: 0;
+      width: 3px;
+      height: 16px;
+      margin-right: 4px;
+      background: @primary-color;
+      content: '';
+    }
+
+    &__help {
+      margin-left: 10px;
+    }
+  }
+</style>

+ 100 - 0
src/components/Breadcrumb/Breadcrumb.vue

@@ -0,0 +1,100 @@
+<template>
+  <div ref="breadcrumbRef" class="breadcrumb">
+    <slot />
+  </div>
+</template>
+
+<script lang="ts">
+  import type { PropType } from 'vue';
+  import { defineComponent, provide, ref } from 'vue';
+
+  export default defineComponent({
+    name: 'Breadcrumb',
+    props: {
+      separator: {
+        type: String as PropType<string>,
+        default: '/',
+      },
+      separatorClass: {
+        type: String as PropType<string>,
+        default: '',
+      },
+    },
+    setup(props) {
+      const breadcrumbRef = ref<Nullable<HTMLElement>>(null);
+
+      provide('breadcrumb', props);
+
+      return {
+        breadcrumbRef,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @import (reference) '../../design/index.less';
+
+  .breadcrumb {
+    height: @header-height;
+    padding-right: 20px;
+    font-size: 14px;
+    line-height: @header-height;
+    // line-height: 1;
+
+    &::after,
+    &::before {
+      display: table;
+      content: '';
+    }
+
+    &::after {
+      clear: both;
+    }
+
+    &__separator {
+      margin: 0 9px;
+      font-weight: 700;
+      color: @breadcrumb-item-normal-color;
+
+      &[class*='icon'] {
+        margin: 0 6px;
+        font-weight: 400;
+      }
+    }
+
+    &__item {
+      float: left;
+    }
+
+    &__inner {
+      color: @breadcrumb-item-normal-color;
+
+      &.is-link,
+      a {
+        font-weight: 700;
+        color: @text-color-base;
+        text-decoration: none;
+        transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+      }
+
+      a:hover,
+      &.is-link:hover {
+        color: @primary-color;
+        cursor: pointer;
+      }
+    }
+
+    &__item:last-child .breadcrumb__inner,
+    &__item:last-child &__inner a,
+    &__item:last-child &__inner a:hover,
+    &__item:last-child &__inner:hover {
+      font-weight: 400;
+      color: @breadcrumb-item-normal-color;
+      cursor: text;
+    }
+
+    &__item:last-child &__separator {
+      display: none;
+    }
+  }
+</style>

+ 62 - 0
src/components/Breadcrumb/BreadcrumbItem.vue

@@ -0,0 +1,62 @@
+<template>
+  <span class="breadcrumb__item">
+    <span ref="linkRef" :class="['breadcrumb__inner', to || isLink ? 'is-link' : '']">
+      <slot />
+    </span>
+    <i v-if="separatorClass" class="breadcrumb__separator" :class="separatorClass"></i>
+    <span v-else class="breadcrumb__separator">{{ separator }}</span>
+  </span>
+</template>
+
+<script lang="ts">
+  import { defineComponent, inject, ref, onMounted, unref } from 'vue';
+  import { useRouter } from 'vue-router';
+  import { useEvent } from '/@/hooks/event/useEvent';
+
+  export default defineComponent({
+    name: 'BreadcrumbItem',
+    props: {
+      to: {
+        type: [String, Object],
+        default: '',
+      },
+      replace: {
+        type: Boolean,
+        default: false,
+      },
+      isLink: {
+        type: Boolean,
+        default: false,
+      },
+    },
+    setup(props) {
+      const linkRef = ref<Nullable<HTMLElement>>(null);
+      const parent = inject('breadcrumb') as {
+        separator: string;
+        separatorClass: string;
+      };
+      const { push, replace } = useRouter();
+
+      onMounted(() => {
+        const link = unref(linkRef);
+        if (!link) return;
+        useEvent({
+          el: link,
+          listener: () => {
+            const { to } = props;
+            if (!props.to) return;
+            props.replace ? replace(to) : push(to);
+          },
+          name: 'click',
+          wait: 0,
+        });
+      });
+
+      return {
+        linkRef,
+        separator: parent.separator && parent.separator,
+        separatorClass: parent.separatorClass && parent.separatorClass,
+      };
+    },
+  });
+</script>

+ 88 - 0
src/components/Button/index.vue

@@ -0,0 +1,88 @@
+<template>
+  <Button v-bind="getBindValue" :class="[getColor, $attrs.class]">
+    <template v-slot:[item] v-for="item in Object.keys($slots)">
+      <slot :name="item" />
+    </template>
+  </Button>
+</template>
+<script lang="ts">
+  import { PropType } from 'vue';
+
+  import { defineComponent, computed, unref } from 'vue';
+  import { Button } from 'ant-design-vue';
+  // import { extendSlots } from '/@/utils/helper/tsxHelper';
+  import { useThrottle } from '/@/hooks/core/useThrottle';
+  import { isFunction } from '/@/utils/is';
+  export default defineComponent({
+    name: 'AButton',
+    inheritAttrs: false,
+    components: { Button },
+    props: {
+      // 按钮类型
+      type: {
+        type: String as PropType<'primary' | 'default' | 'danger' | 'dashed' | 'link'>,
+        default: 'default',
+      },
+      // 节流防抖类型 throttle debounce
+      throttle: {
+        type: String as PropType<'throttle' | 'debounce'>,
+        default: 'throttle',
+      },
+      color: {
+        type: String as PropType<'error' | 'warning' | 'success'>,
+      },
+      // 防抖节流时间
+      throttleTime: {
+        type: Number as PropType<number>,
+        default: 0,
+      },
+      loading: {
+        type: Boolean as PropType<boolean>,
+        default: false,
+      },
+      disabled: {
+        type: Boolean as PropType<boolean>,
+        default: false,
+      },
+    },
+    setup(props, { attrs }) {
+      const getListeners = computed(() => {
+        const { throttle, throttleTime = 0 } = props;
+        // 是否开启节流防抖
+        const throttleType = throttle!.toLowerCase();
+        const isDebounce = throttleType === 'debounce';
+        const openThrottle = ['throttle', 'debounce'].includes(throttleType) && throttleTime > 0;
+
+        const on: {
+          onClick?: Fn;
+        } = {};
+
+        if (attrs.onClick && isFunction(attrs.onClick) && openThrottle) {
+          const [handler] = useThrottle(attrs.onClick as any, throttleTime!, {
+            debounce: isDebounce,
+            immediate: true,
+          });
+          on.onClick = handler;
+        }
+
+        return {
+          ...attrs,
+          ...on,
+        };
+      });
+
+      const getColor = computed(() => {
+        const res: string[] = [];
+        const { color, disabled } = props;
+        color && res.push(`ant-btn-${color}`);
+        disabled && res.push('is-disabled');
+        return res;
+      });
+
+      const getBindValue = computed((): any => {
+        return { ...unref(getListeners), ...props };
+      });
+      return { getBindValue, getColor };
+    },
+  });
+</script>

+ 66 - 0
src/components/Button/types.ts

@@ -0,0 +1,66 @@
+import { VNodeChild } from 'vue';
+
+export interface BasicButtonProps {
+  /**
+   * can be set to primary ghost dashed danger(added in 2.7) or omitted (meaning default)
+   * @default 'default'
+   * @type string
+   */
+  type?: 'primary' | 'danger' | 'dashed' | 'ghost' | 'default';
+
+  /**
+   * set the original html type of button
+   * @default 'button'
+   * @type string
+   */
+  htmlType?: 'button' | 'submit' | 'reset' | 'menu';
+
+  /**
+   * set the icon of button
+   * @type string
+   */
+  icon?: VNodeChild | JSX.Element;
+
+  /**
+   * can be set to circle or circle-outline or omitted
+   * @type string
+   */
+  shape?: 'circle' | 'circle-outline';
+
+  /**
+   * can be set to small large or omitted
+   * @default 'default'
+   * @type string
+   */
+  size?: 'small' | 'large' | 'default';
+
+  /**
+   * set the loading status of button
+   * @default false
+   * @type boolean | { delay: number }
+   */
+  loading?: boolean | { delay: number };
+
+  /**
+   * disabled state of button
+   * @default false
+   * @type boolean
+   */
+  disabled?: boolean;
+
+  /**
+   * make background transparent and invert text and border colors, added in 2.7
+   * @default false
+   * @type boolean
+   */
+  ghost?: boolean;
+
+  /**
+   * option to fit button width to its parent width
+   * @default false
+   * @type boolean
+   */
+  block?: boolean;
+
+  onClick?: (e?: Event) => void;
+}

+ 21 - 0
src/components/ClickOutSide/index.vue

@@ -0,0 +1,21 @@
+<template>
+  <div ref="wrapRef"><slot /></div>
+</template>
+<script lang="ts">
+  import type { Ref } from 'vue';
+  import { defineComponent, ref } from 'vue';
+
+  import { useClickOutside } from '/@/hooks/web/useClickOutside';
+
+  export default defineComponent({
+    name: 'ClickOutSide',
+
+    setup(_, { emit }) {
+      const wrapRef = ref<Nullable<HTMLDivElement | null>>(null);
+      useClickOutside(wrapRef as Ref<HTMLDivElement>, () => {
+        emit('clickOutside');
+      });
+      return { wrapRef };
+    },
+  });
+</script>

+ 5 - 0
src/components/Container/index.ts

@@ -0,0 +1,5 @@
+export { default as ScrollContainer } from './src/ScrollContainer.vue';
+export { default as CollapseContainer } from './src/collapse/CollapseContainer.vue';
+export { default as LazyContainer } from './src/LazyContainer';
+
+export * from './src/types.d';

+ 27 - 0
src/components/Container/src/LazyContainer.less

@@ -0,0 +1,27 @@
+.lazy-container-enter {
+  opacity: 0;
+}
+
+.lazy-container-enter-to {
+  opacity: 1;
+}
+
+.lazy-container-enter-from,
+.lazy-container-enter-active {
+  position: absolute;
+  top: 0;
+  width: 100%;
+  transition: opacity 0.3s 0.2s;
+}
+
+.lazy-container-leave {
+  opacity: 1;
+}
+
+.lazy-container-leave-to {
+  opacity: 0;
+}
+
+.lazy-container-leave-active {
+  transition: opacity 0.5s;
+}

+ 200 - 0
src/components/Container/src/LazyContainer.tsx

@@ -0,0 +1,200 @@
+import type { PropType } from 'vue';
+
+import {
+  defineComponent,
+  reactive,
+  onMounted,
+  ref,
+  unref,
+  onUnmounted,
+  TransitionGroup,
+} from 'vue';
+
+import { Skeleton } from 'ant-design-vue';
+import { useRaf } from '/@/hooks/event/useRaf';
+import { useTimeout } from '/@/hooks/core/useTimeout';
+import { getListeners, getSlot } from '/@/utils/helper/tsxHelper';
+
+import './LazyContainer.less';
+
+interface State {
+  isInit: boolean;
+  loading: boolean;
+  intersectionObserverInstance: IntersectionObserver | null;
+}
+export default defineComponent({
+  name: 'LazyContainer',
+  emits: ['before-init', 'init'],
+  props: {
+    // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
+    timeout: {
+      type: Number as PropType<number>,
+      default: 8000,
+      // default: 8000,
+    },
+    // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
+    viewport: {
+      type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>,
+      default: () => null,
+    },
+    // 预加载阈值, css单位
+    threshold: {
+      type: String as PropType<string>,
+      default: '0px',
+    },
+
+    // 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向
+    direction: {
+      type: String as PropType<'vertical' | 'horizontal'>,
+      default: 'vertical',
+    },
+    // 包裹组件的外层容器的标签名
+    tag: {
+      type: String as PropType<string>,
+      default: 'div',
+    },
+
+    maxWaitingTime: {
+      type: Number as PropType<number>,
+      default: 80,
+    },
+
+    // 是否在不可见的时候销毁
+    autoDestory: {
+      type: Boolean as PropType<boolean>,
+      default: false,
+    },
+
+    // transition name
+    transitionName: {
+      type: String as PropType<string>,
+      default: 'lazy-container',
+    },
+  },
+  setup(props, { attrs, emit, slots }) {
+    const elRef = ref<any>(null);
+    const state = reactive<State>({
+      isInit: false,
+      loading: false,
+      intersectionObserverInstance: null,
+    });
+
+    // If there is a set delay time, it will be executed immediately
+    function immediateInit() {
+      const { timeout } = props;
+      timeout &&
+        useTimeout(() => {
+          init();
+        }, timeout);
+    }
+
+    function init() {
+      // At this point, the skeleton component is about to be switched
+      emit('before-init');
+      // At this point you can prepare to load the resources of the lazy-loaded component
+      state.loading = true;
+
+      requestAnimationFrameFn(() => {
+        state.isInit = true;
+        emit('init');
+      });
+    }
+    function requestAnimationFrameFn(callback: () => any) {
+      // Prevent waiting too long without executing the callback
+      // Set the maximum waiting time
+      useTimeout(() => {
+        if (state.isInit) {
+          return;
+        }
+        callback();
+      }, props.maxWaitingTime || 80);
+
+      const { requestAnimationFrame } = useRaf();
+
+      return requestAnimationFrame;
+    }
+    function initIntersectionObserver() {
+      const { timeout, direction, threshold, viewport } = props;
+      if (timeout) {
+        return;
+      }
+      // According to the scrolling direction to construct the viewport margin, used to load in advance
+      let rootMargin;
+      switch (direction) {
+        case 'vertical':
+          rootMargin = `${threshold} 0px`;
+          break;
+        case 'horizontal':
+          rootMargin = `0px ${threshold}`;
+          break;
+      }
+      try {
+        // Observe the intersection of the viewport and the component container
+        state.intersectionObserverInstance = new window.IntersectionObserver(intersectionHandler, {
+          rootMargin,
+          root: viewport,
+          threshold: [0, Number.MIN_VALUE, 0.01],
+        });
+
+        const el = unref(elRef);
+
+        state.intersectionObserverInstance.observe(el.$el);
+      } catch (e) {
+        init();
+      }
+    }
+    // Cross-condition change handling function
+    function intersectionHandler(entries: any[]) {
+      const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
+      if (isIntersecting) {
+        init();
+        if (state.intersectionObserverInstance) {
+          const el = unref(elRef);
+          state.intersectionObserverInstance.unobserve(el.$el);
+        }
+      }
+      // else {
+      //   const { autoDestory } = props;
+      //   autoDestory && destory();
+      // }
+    }
+    // function destory() {
+    //   emit('beforeDestory');
+    //   state.loading = false;
+    //   nextTick(() => {
+    //     emit('destory');
+    //   });
+    // }
+
+    immediateInit();
+    onMounted(() => {
+      initIntersectionObserver();
+    });
+    onUnmounted(() => {
+      // Cancel the observation before the component is destroyed
+      if (state.intersectionObserverInstance) {
+        const el = unref(elRef);
+        state.intersectionObserverInstance.unobserve(el.$el);
+      }
+    });
+
+    function renderContent() {
+      const { isInit, loading } = state;
+      if (isInit) {
+        return <div key="component">{getSlot(slots, 'default', { loading })}</div>;
+      }
+      if (slots.skeleton) {
+        return <div key="skeleton">{getSlot(slots, 'skeleton') || <Skeleton />}</div>;
+      }
+      return null;
+    }
+    return () => {
+      const { tag, transitionName } = props;
+      return (
+        <TransitionGroup ref={elRef} name={transitionName} tag={tag} {...getListeners(attrs)}>
+          {() => renderContent()}
+        </TransitionGroup>
+      );
+    };
+  },
+});

+ 80 - 0
src/components/Container/src/ScrollContainer.vue

@@ -0,0 +1,80 @@
+<template>
+  <Scrollbar
+    ref="scrollbarRef"
+    :wrapClass="`scrollbar__wrap`"
+    :viewClass="`scrollbar__view`"
+    class="scroll-container"
+  >
+    <slot />
+  </Scrollbar>
+</template>
+
+<script lang="ts">
+  // component
+  import { defineComponent, ref, unref, nextTick } from 'vue';
+  import { Scrollbar } from '/@/components/Scrollbar';
+
+  // hook
+  import { useScrollTo } from '/@/hooks/event/useScrollTo';
+
+  export default defineComponent({
+    name: 'ScrollContainer',
+    components: { Scrollbar },
+    setup() {
+      const scrollbarRef = ref<RefInstanceType<any>>(null);
+
+      function scrollTo(to: number, duration = 500) {
+        const scrollbar = unref(scrollbarRef);
+        if (!scrollbar) return;
+        nextTick(() => {
+          const { start } = useScrollTo({
+            el: unref(scrollbar.$.wrap),
+            to,
+            duration,
+          });
+          start();
+        });
+      }
+
+      function getScrollWrap() {
+        const scrollbar = unref(scrollbarRef);
+        if (!scrollbar) return null;
+        return scrollbar.$.wrap;
+      }
+
+      function scrollBottom() {
+        const scrollbar = unref(scrollbarRef);
+        if (!scrollbar) return;
+        nextTick(() => {
+          const scrollHeight = scrollbar.$.wrap.scrollHeight as number;
+          const { start } = useScrollTo({
+            el: unref(scrollbar.$.wrap),
+            to: scrollHeight,
+          });
+          start();
+        });
+      }
+      return {
+        scrollbarRef,
+        scrollTo,
+        scrollBottom,
+        getScrollWrap,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  .scroll-container {
+    width: 100%;
+    height: 100%;
+
+    .scrollbar__wrap {
+      margin-bottom: 18px !important;
+      overflow-x: hidden;
+    }
+
+    .scrollbar__view {
+      box-sizing: border-box;
+    }
+  }
+</style>

+ 117 - 0
src/components/Container/src/collapse/CollapseContainer.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="collapse-container p-2 bg:white rounded-sm">
+    <CollapseHeader v-bind="$props" :show="show" @expand="handleExpand" />
+    <CollapseTransition :enable="canExpan">
+      <Skeleton v-if="loading" />
+      <div class="collapse-container__body" v-else v-show="show">
+        <LazyContainer :timeout="lazyTime" v-if="lazy">
+          <slot />
+          <template v-slot:skeleton>
+            <slot name="lazySkeleton" />
+          </template>
+        </LazyContainer>
+        <slot />
+      </div>
+    </CollapseTransition>
+  </div>
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+
+  import { defineComponent, ref, unref } from 'vue';
+  // component
+  import { CollapseTransition } from '/@/components/Transition/index';
+  import CollapseHeader from './CollapseHeader.vue';
+  import { Skeleton } from 'ant-design-vue';
+
+  import LazyContainer from '../LazyContainer';
+
+  import { triggerWindowResize } from '/@/utils/event/triggerWindowResizeEvent';
+  // hook
+  import { useTimeout } from '/@/hooks/core/useTimeout';
+  export default defineComponent({
+    components: { Skeleton, LazyContainer, CollapseHeader, CollapseTransition },
+    name: 'CollapseContainer',
+    props: {
+      // 标题
+      title: {
+        type: String as PropType<string>,
+        default: '',
+      },
+      // 是否可以展开
+      canExpan: {
+        type: Boolean as PropType<boolean>,
+        default: true,
+      },
+      // 标题右侧温馨提醒
+      helpMessage: {
+        type: [Array, String] as PropType<string[] | string>,
+        default: '',
+      },
+      // 展开收缩的时候是否触发window.resize,
+      // 可以适应表格和表单,当表单收缩起来,表格触发resize 自适应高度
+      triggerWindowResize: {
+        type: Boolean as PropType<boolean>,
+        default: false,
+      },
+      loading: {
+        type: Boolean as PropType<boolean>,
+        default: false,
+      },
+      // 延时加载
+      lazy: {
+        type: Boolean as PropType<boolean>,
+        default: false,
+      },
+      // 延时加载时间
+      lazyTime: {
+        type: Number as PropType<number>,
+        default: 3000,
+      },
+    },
+    setup(props) {
+      const showRef = ref(true);
+      /**
+       * @description: 处理开展事件
+       */
+      function handleExpand() {
+        const hasShow = !unref(showRef);
+        showRef.value = hasShow;
+
+        if (props.triggerWindowResize) {
+          // 这里200毫秒是因为展开有动画,
+          useTimeout(triggerWindowResize, 200);
+        }
+      }
+      return {
+        show: showRef,
+        handleExpand,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  .collapse-container {
+    padding: 10px;
+    background: #fff;
+    border-radius: 8px;
+    transition: all 0.3s ease-in-out;
+
+    &.no-shadow {
+      box-shadow: none;
+    }
+
+    &__header {
+      display: flex;
+      height: 32px;
+      margin-bottom: 10px;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    &__action {
+      display: flex;
+      align-items: center;
+    }
+  }
+</style>

+ 32 - 0
src/components/Container/src/collapse/CollapseHeader.vue

@@ -0,0 +1,32 @@
+<template>
+  <div class="collapse-container__header">
+    <BasicTitle :helpMessage="$attrs.helpMessage">
+      <template v-if="$attrs.title">
+        {{ $attrs.title }}
+      </template>
+      <template v-else>
+        <slot name="title" />
+      </template>
+    </BasicTitle>
+
+    <div class="collapse-container__action">
+      <slot name="action" />
+      <BasicArrow v-if="$attrs.canExpan" :expand="$attrs.show" @click="handleExpand" />
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { BasicArrow } from '/@/components/Basic';
+  import { BasicTitle } from '/@/components/Basic';
+  export default defineComponent({
+    inheritAttrs: false,
+    components: { BasicArrow, BasicTitle },
+    setup(_, { emit }) {
+      function handleExpand() {
+        emit('expand');
+      }
+      return { handleExpand };
+    },
+  });
+</script>

+ 17 - 0
src/components/Container/src/types.d.ts

@@ -0,0 +1,17 @@
+export type ScrollType = 'default' | 'main';
+
+export interface CollapseContainerOptions {
+  canExpand?: boolean;
+  title?: string;
+  helpMessage?: Array<any> | string;
+}
+export interface ScrollContainerOptions {
+  enableScroll?: boolean;
+  type?: ScrollType;
+}
+
+export type ScrollActionType = RefType<{
+  scrollBottom: () => void;
+  getScrollWrap: () => Nullable<HTMLElement>;
+  scrollTo: (top: number) => void;
+}>;

+ 65 - 0
src/components/ContextMenu/index.ts

@@ -0,0 +1,65 @@
+import contextMenuVue from './src/index';
+import { isClient } from '/@/utils/is';
+import { Options, Props } from './src/types';
+import { createApp } from 'vue';
+const menuManager: {
+  doms: Element[];
+  resolve: Fn;
+} = {
+  doms: [],
+  resolve: () => {},
+};
+export const createContextMenu = function (options: Options) {
+  const { event } = options || {};
+  try {
+    event.preventDefault();
+  } catch (e) {
+    console.log(e);
+  }
+
+  if (!isClient) return;
+  return new Promise((resolve) => {
+    const wrapDom = document.createElement('div');
+    const propsData: Partial<Props> = {};
+    if (options.styles !== undefined) propsData.styles = options.styles;
+    if (options.items !== undefined) propsData.items = options.items;
+    if (options.event !== undefined) {
+      propsData.customEvent = event;
+      propsData.axis = { x: event.clientX, y: event.clientY };
+    }
+    createApp(contextMenuVue, propsData).mount(wrapDom);
+    const bodyClick = function () {
+      menuManager.resolve('');
+    };
+    const contextMenuDom = wrapDom.children[0];
+    menuManager.doms.push(contextMenuDom);
+    const remove = function () {
+      menuManager.doms.forEach((dom: Element) => {
+        try {
+          document.body.removeChild(dom);
+        } catch (error) {}
+      });
+      document.body.removeEventListener('click', bodyClick);
+      document.body.removeEventListener('scroll', bodyClick);
+      try {
+        (wrapDom as any) = null;
+      } catch (error) {}
+    };
+    menuManager.resolve = function (...arg: any) {
+      resolve(arg[0]);
+      remove();
+    };
+    remove();
+    document.body.appendChild(contextMenuDom);
+    document.body.addEventListener('click', bodyClick);
+    document.body.addEventListener('scroll', bodyClick);
+  });
+};
+export const unMountedContextMenu = function () {
+  if (menuManager) {
+    menuManager.resolve('');
+    menuManager.doms = [];
+  }
+};
+
+export * from './src/types';

+ 49 - 0
src/components/ContextMenu/src/index.less

@@ -0,0 +1,49 @@
+@import (reference) '../../../design/index.less';
+
+.context-menu {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 1500;
+  display: block;
+  width: 156px;
+  min-width: 10rem;
+  margin: 0;
+  list-style: none;
+  background-color: #fff;
+  border: 1px solid rgba(0, 0, 0, 0.08);
+  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);
+  background-clip: padding-box;
+  user-select: none;
+
+  &.hidden {
+    display: none !important;
+  }
+
+  &__item {
+    a {
+      display: inline-block;
+      width: 100%;
+      padding: 10px 14px;
+
+      &:hover {
+        color: @text-color-base;
+        background: #eee;
+      }
+    }
+
+    &.disabled {
+      a {
+        color: @disabled-color;
+        cursor: not-allowed;
+
+        &:hover {
+          color: @disabled-color;
+          background: unset;
+        }
+      }
+    }
+  }
+}

+ 90 - 0
src/components/ContextMenu/src/index.tsx

@@ -0,0 +1,90 @@
+import {
+  defineComponent,
+  nextTick,
+  onMounted,
+  reactive,
+  computed,
+  ref,
+  unref,
+  onUnmounted,
+} from 'vue';
+import { props } from './props';
+import Icon from '/@/components/Icon';
+import type { ContextMenuItem } from './types';
+import './index.less';
+const prefixCls = 'context-menu';
+export default defineComponent({
+  name: 'ContextMenu',
+  props,
+  setup(props) {
+    const wrapRef = ref<Nullable<HTMLDivElement>>(null);
+    const state = reactive({
+      show: false,
+    });
+    onMounted(() => {
+      nextTick(() => {
+        state.show = true;
+      });
+    });
+    onUnmounted(() => {
+      const el = unref(wrapRef);
+      el && document.body.removeChild(el);
+    });
+    const getStyle = computed(() => {
+      const { axis, items, styles, width } = props;
+      const { x, y } = axis || { x: 0, y: 0 };
+      const menuHeight = (items || []).length * 40;
+      const menuWidth = width;
+      const body = document.body;
+      return {
+        ...(styles as any),
+        width: `${width}px`,
+        left: (body.clientWidth < x + menuWidth ? x - menuWidth : x) + 'px',
+        top: (body.clientHeight < y + menuHeight ? y - menuHeight : y) + 'px',
+      };
+    });
+    function handleAction(item: ContextMenuItem, e: MouseEvent) {
+      const { handler, disabled } = item;
+      if (disabled) {
+        return;
+      }
+      state.show = false;
+      if (e) {
+        e.stopPropagation();
+        e.preventDefault();
+      }
+
+      handler && handler();
+    }
+    function renderContent(item: ContextMenuItem) {
+      const { icon, label } = item;
+
+      const { showIcon } = props;
+      return (
+        <span style="display: inline-block; width: 100%;">
+          {showIcon && icon && <Icon class="mr-2" icon={icon} />}
+          <span>{label}</span>
+        </span>
+      );
+    }
+    function renderMenuItem(items: ContextMenuItem[]) {
+      return items.map((item) => {
+        const { disabled, label } = item;
+
+        return (
+          <li class={`${prefixCls}__item ${disabled ? 'disabled' : ''}`} key={label}>
+            <a onClick={handleAction.bind(null, item)}>{renderContent(item)}</a>
+          </li>
+        );
+      });
+    }
+    return () => {
+      const { items } = props;
+      return (
+        <ul class={[prefixCls, !state.show && 'hidden']} ref={wrapRef} style={unref(getStyle)}>
+          {renderMenuItem(items)}
+        </ul>
+      );
+    };
+  },
+});

+ 40 - 0
src/components/ContextMenu/src/props.ts

@@ -0,0 +1,40 @@
+import type { PropType } from 'vue';
+import type { Axis, ContextMenuItem } from './types';
+export const props = {
+  width: {
+    type: Number as PropType<number>,
+    default: 180,
+  },
+  customEvent: {
+    type: Object as PropType<Event>,
+    default: null,
+  },
+  // 样式
+  styles: {
+    type: Object as PropType<any>,
+    default: null,
+  },
+  showIcon: {
+    // 是否显示icon
+    type: Boolean as PropType<boolean>,
+    default: true,
+  },
+  axis: {
+    // 鼠标右键点击的位置
+    type: Object as PropType<Axis>,
+    default() {
+      return { x: 0, y: 0 };
+    },
+  },
+  items: {
+    // 最重要的列表,没有的话直接不显示
+    type: Array as PropType<ContextMenuItem[]>,
+    default() {
+      return [];
+    },
+  },
+  resolve: {
+    type: Function as PropType<any>,
+    default: null,
+  },
+};

+ 30 - 0
src/components/ContextMenu/src/types.ts

@@ -0,0 +1,30 @@
+export interface Axis {
+  x: number;
+  y: number;
+}
+
+export interface ContextMenuItem {
+  label: string;
+  icon?: string;
+  disabled?: boolean;
+  handler?: Fn;
+  divider?: boolean;
+  children?: ContextMenuItem[];
+}
+export interface Options {
+  event: MouseEvent;
+  icon?: string;
+  styles?: any;
+  items?: ContextMenuItem[];
+}
+
+export type Props = {
+  resolve?: (...arg: any) => void;
+  event?: MouseEvent;
+  styles?: any;
+  items: ContextMenuItem[];
+  customEvent?: MouseEvent;
+  axis?: Axis;
+  width?: number;
+  showIcon?: boolean;
+};

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.