소스 검색

提交测试

tangle 2 년 전
부모
커밋
6272824f66
100개의 변경된 파일4183개의 추가작업 그리고 0개의 파일을 삭제
  1. 3 0
      simulation-ui/.eslintignore
  2. 70 0
      simulation-ui/.eslintrc.js
  3. 16 0
      simulation-ui/.gitattributes
  4. 6 0
      simulation-ui/.gitignore
  5. 8 0
      simulation-ui/.idea/.gitignore
  6. 5 0
      simulation-ui/.idea/codeStyles/codeStyleConfig.xml
  7. 7 0
      simulation-ui/.idea/inspectionProfiles/Project_Default.xml
  8. 6 0
      simulation-ui/.idea/misc.xml
  9. 8 0
      simulation-ui/.idea/modules.xml
  10. 6 0
      simulation-ui/.idea/vcs.xml
  11. 8 0
      simulation-ui/.prettierrc.js
  12. 29 0
      simulation-ui/.stylelintrc.js
  13. 54 0
      simulation-ui/.vscode/settings.json
  14. 31 0
      simulation-ui/README.md
  15. 3 0
      simulation-ui/babel.config.js
  16. 3 0
      simulation-ui/commitlint.config.js
  17. 34 0
      simulation-ui/config/vite.config.base.ts
  18. 23 0
      simulation-ui/config/vite.config.dev.ts
  19. 9 0
      simulation-ui/config/vite.config.prod.ts
  20. 13 0
      simulation-ui/index.html
  21. 76 0
      simulation-ui/package.json
  22. 36 0
      simulation-ui/src/App.vue
  23. 21 0
      simulation-ui/src/api/dashboard.ts
  24. 21 0
      simulation-ui/src/api/form.ts
  25. 60 0
      simulation-ui/src/api/interceptor.ts
  26. 56 0
      simulation-ui/src/api/list.ts
  27. 38 0
      simulation-ui/src/api/message.ts
  28. 49 0
      simulation-ui/src/api/profile.ts
  29. 77 0
      simulation-ui/src/api/user-center.ts
  30. 22 0
      simulation-ui/src/api/user.ts
  31. 73 0
      simulation-ui/src/api/visualization.ts
  32. BIN
      simulation-ui/src/assets/images/login-banner.png
  33. 12 0
      simulation-ui/src/assets/logo.svg
  34. 44 0
      simulation-ui/src/assets/style/custom.less
  35. 104 0
      simulation-ui/src/assets/style/global.less
  36. 0 0
      simulation-ui/src/assets/world.json
  37. 37 0
      simulation-ui/src/components/breadcrumb/index.vue
  38. 58 0
      simulation-ui/src/components/chart/index.vue
  39. 31 0
      simulation-ui/src/components/footer/index.vue
  40. 79 0
      simulation-ui/src/components/global-setting/block.vue
  41. 48 0
      simulation-ui/src/components/global-setting/form-wrapper.vue
  42. 71 0
      simulation-ui/src/components/global-setting/index.vue
  43. 37 0
      simulation-ui/src/components/index.ts
  44. 166 0
      simulation-ui/src/components/menu/index.vue
  45. 147 0
      simulation-ui/src/components/message-box/index.vue
  46. 155 0
      simulation-ui/src/components/message-box/list.vue
  47. 13 0
      simulation-ui/src/components/message-box/locale/en-US.ts
  48. 13 0
      simulation-ui/src/components/message-box/locale/zh-CN.ts
  49. 309 0
      simulation-ui/src/components/navbar/index.vue
  50. 11 0
      simulation-ui/src/config/settings.json
  51. 8 0
      simulation-ui/src/directive/index.ts
  52. 30 0
      simulation-ui/src/directive/permission/index.ts
  53. 8 0
      simulation-ui/src/env.d.ts
  54. 27 0
      simulation-ui/src/hooks/chart-option.ts
  55. 16 0
      simulation-ui/src/hooks/loading.ts
  56. 19 0
      simulation-ui/src/hooks/locale.ts
  57. 33 0
      simulation-ui/src/hooks/permission.ts
  58. 26 0
      simulation-ui/src/hooks/request.ts
  59. 12 0
      simulation-ui/src/hooks/themes.ts
  60. 24 0
      simulation-ui/src/hooks/user.ts
  61. 214 0
      simulation-ui/src/layout/page-layout.vue
  62. 63 0
      simulation-ui/src/locale/en-US.ts
  63. 24 0
      simulation-ui/src/locale/en-US/settings.ts
  64. 21 0
      simulation-ui/src/locale/index.ts
  65. 63 0
      simulation-ui/src/locale/zh-CN.ts
  66. 24 0
      simulation-ui/src/locale/zh-CN/settings.ts
  67. 26 0
      simulation-ui/src/main.ts
  68. 26 0
      simulation-ui/src/mock/index.ts
  69. 85 0
      simulation-ui/src/mock/message-box.ts
  70. 69 0
      simulation-ui/src/mock/user.ts
  71. 95 0
      simulation-ui/src/router/index.ts
  72. 34 0
      simulation-ui/src/router/modules/dashboard.ts
  73. 42 0
      simulation-ui/src/router/modules/exception.ts
  74. 32 0
      simulation-ui/src/router/modules/form.ts
  75. 23 0
      simulation-ui/src/router/modules/index.ts
  76. 32 0
      simulation-ui/src/router/modules/list.ts
  77. 9 0
      simulation-ui/src/router/modules/login.ts
  78. 22 0
      simulation-ui/src/router/modules/profile.ts
  79. 32 0
      simulation-ui/src/router/modules/result.ts
  80. 32 0
      simulation-ui/src/router/modules/user.ts
  81. 33 0
      simulation-ui/src/router/modules/visualization.ts
  82. 15 0
      simulation-ui/src/router/typings.d.ts
  83. 8 0
      simulation-ui/src/store/index.ts
  84. 34 0
      simulation-ui/src/store/modules/app/index.ts
  85. 12 0
      simulation-ui/src/store/modules/app/types.ts
  86. 82 0
      simulation-ui/src/store/modules/user/index.ts
  87. 19 0
      simulation-ui/src/store/modules/user/types.ts
  88. 10 0
      simulation-ui/src/types/echarts.ts
  89. 37 0
      simulation-ui/src/types/global.ts
  90. 5 0
      simulation-ui/src/types/mock.ts
  91. 17 0
      simulation-ui/src/utils/auth.ts
  92. 3 0
      simulation-ui/src/utils/env.ts
  93. 28 0
      simulation-ui/src/utils/monitor.ts
  94. 23 0
      simulation-ui/src/utils/setup-mock.ts
  95. 9 0
      simulation-ui/src/views/dashboard/index.vue
  96. 96 0
      simulation-ui/src/views/dashboard/monitor/components/chat-item.vue
  97. 81 0
      simulation-ui/src/views/dashboard/monitor/components/chat-list.vue
  98. 90 0
      simulation-ui/src/views/dashboard/monitor/components/chat-panel.vue
  99. 141 0
      simulation-ui/src/views/dashboard/monitor/components/data-statistic-list.vue
  100. 63 0
      simulation-ui/src/views/dashboard/monitor/components/data-statistic.vue

+ 3 - 0
simulation-ui/.eslintignore

@@ -0,0 +1,3 @@
+/*.json
+/*.js
+dist

+ 70 - 0
simulation-ui/.eslintrc.js

@@ -0,0 +1,70 @@
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const path = require('path');
+
+module.exports = {
+  root: true,
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    // Parser that checks the content of the <script> tag
+    parser: '@typescript-eslint/parser',
+    sourceType: 'module',
+    ecmaVersion: 2020,
+    ecmaFeatures: {
+      jsx: true,
+    },
+  },
+  env: {
+    browser: true,
+    node: true,
+  },
+  plugins: ['@typescript-eslint'],
+  extends: [
+    // Airbnb JavaScript Style Guide https://github.com/airbnb/javascript
+    'airbnb-base',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:import/recommended',
+    'plugin:import/typescript',
+    'plugin:vue/vue3-recommended',
+    'plugin:prettier/recommended',
+  ],
+  settings: {
+    'import/resolver': {
+      typescript: {
+        project: path.resolve(__dirname, './tsconfig.json'),
+      },
+    },
+  },
+  rules: {
+    'prettier/prettier': 1,
+    'vue/no-reserved-component-names': 0,
+    // Vue: Recommended rules to be closed or modify
+    'vue/require-default-prop': 0,
+    'vue/singleline-html-element-content-newline': 0,
+    'vue/max-attributes-per-line': 0,
+    // Vue: Add extra rules
+    'vue/custom-event-name-casing': [2, 'camelCase'],
+    'vue/no-v-text': 1,
+    'vue/padding-line-between-blocks': 1,
+    'vue/require-direct-export': 1,
+    'vue/multi-word-component-names': 0,
+    // Allow @ts-ignore comment
+    '@typescript-eslint/ban-ts-comment': 0,
+    '@typescript-eslint/no-unused-vars': 1,
+    '@typescript-eslint/no-empty-function': 1,
+    '@typescript-eslint/no-explicit-any': 0,
+    'import/extensions': [
+      2,
+      'ignorePackages',
+      {
+        js: 'never',
+        jsx: 'never',
+        ts: 'never',
+        tsx: 'never',
+      },
+    ],
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+    'no-param-reassign': 0,
+    'prefer-regex-literals': 0,
+    'import/no-extraneous-dependencies': 0,
+  },
+};

+ 16 - 0
simulation-ui/.gitattributes

@@ -0,0 +1,16 @@
+*.html text eol=lf
+*.css text eol=lf
+*.js text eol=lf
+*.scss text eol=lf
+*.vue text eol=lf
+*.hbs text eol=lf
+*.sh text eol=lf
+*.md text eol=lf
+*.json text eol=lf
+*.yml text eol=lf
+.browserslistrc text eol=lf
+.editorconfig text eol=lf
+.eslintignore text eol=lf
+.gitattributes text eol=lf
+LICENSE text eol=lf
+*.conf  text eol=lf

+ 6 - 0
simulation-ui/.gitignore

@@ -0,0 +1,6 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+*.history

+ 8 - 0
simulation-ui/.idea/.gitignore

@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Datasource local storage ignored files
+/../../../../:\code\vue-admin-arco\.idea/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/

+ 5 - 0
simulation-ui/.idea/codeStyles/codeStyleConfig.xml

@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
+  </state>
+</component>

+ 7 - 0
simulation-ui/.idea/inspectionProfiles/Project_Default.xml

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

+ 6 - 0
simulation-ui/.idea/misc.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="11" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/out" />
+  </component>
+</project>

+ 8 - 0
simulation-ui/.idea/modules.xml

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

+ 6 - 0
simulation-ui/.idea/vcs.xml

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

+ 8 - 0
simulation-ui/.prettierrc.js

@@ -0,0 +1,8 @@
+module.exports = {
+  tabWidth: 2,
+  semi: true,
+  printWidth: 80,
+  singleQuote: true,
+  quoteProps: 'consistent',
+  htmlWhitespaceSensitivity: 'strict',
+}

+ 29 - 0
simulation-ui/.stylelintrc.js

@@ -0,0 +1,29 @@
+module.exports = {
+  extends: [
+    'stylelint-config-standard',
+    'stylelint-config-rational-order',
+    'stylelint-config-prettier',
+  ],
+  defaultSeverity: 'warning',
+  plugins: ['stylelint-order'],
+  rules: {
+    'at-rule-no-unknown': [
+      true,
+      {
+        ignoreAtRules: ['plugin'],
+      },
+    ],
+    'rule-empty-line-before': [
+      'always',
+      {
+        except: ['after-single-line-comment', 'first-nested'],
+      },
+    ],
+    'selector-pseudo-class-no-unknown': [
+      true,
+      {
+        ignorePseudoClasses: ['deep'],
+      },
+    ],
+  },
+};

+ 54 - 0
simulation-ui/.vscode/settings.json

@@ -0,0 +1,54 @@
+{
+  "[vue]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "editor.quickSuggestions": {
+    "strings": true
+  },
+  "workbench.colorTheme": "One Monokai",
+  "editor.tabSize": 2,
+  "editor.detectIndentation": false,
+  "emmet.triggerExpansionOnTab": true,
+  "editor.formatOnSave": true,
+  "javascript.format.enable": true,
+  "git.enableSmartCommit": true,
+  "git.autofetch": true,
+  "git.confirmSync": false,
+  "[json]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "liveServer.settings.donotShowInfoMsg": true,
+  "explorer.confirmDelete": false,
+  "javascript.updateImportsOnFileMove.enabled": "always",
+  "typescript.updateImportsOnFileMove.enabled": "always",
+  "files.exclude": {
+    "**/.idea": true
+  },
+  "editor.codeActionsOnSave": {
+    "source.fixAll.stylelint": true,
+    "source.fixAll.eslint": true
+  },
+  "[javascript]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[jsonc]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[html]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "editor.suggest.snippetsPreventQuickSuggestions": false,
+  "prettier.htmlWhitespaceSensitivity": "ignore",
+  "prettier.vueIndentScriptAndStyle": true,
+  "docthis.authorName": "https://gitee.com/chu1204505056/vue-admin-better",
+  "docthis.includeAuthorTag": true,
+  "docthis.includeDescriptionTag": true,
+  "docthis.enableHungarianNotationEvaluation": true,
+  "docthis.inferTypesFromNames": true,
+  "vetur.format.defaultFormatter.html": "prettier",
+  "files.autoSave": "onFocusChange",
+  "path-intellisense.mappings": {
+    "@": "${workspaceRoot}/src"
+  },
+  "files.eol": "\n"
+}

+ 31 - 0
simulation-ui/README.md

@@ -0,0 +1,31 @@
+<div align="center">
+  <h1>vue-admin-arco</h1>
+</div>
+
+<div align="center">
+
+本模板基于 [arco-design-pro-vue](https://arco.design/) 源码魔改而来,Arco Design 是一款由字节跳动出品的让人眼前一亮的 UI 库。
+
+[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/arco-design/arco-design-pro/blob/main/LICENSE)
+
+</div>
+
+## 🔗 演示地址
+
+- [vue-admin-arco 魔改版](https://vue-admin-beautiful.com/vue-admin-arco)
+
+## ✨ 魔改内容
+
+- 重构主题
+- 升级最新版本
+- 未完待续
+
+## 📝 声明
+
+- 本项目目前绝大多数源码均源自字节跳动 Arco Design 开源仓库
+- 本项目仅用于个人学习用途
+
+## 🌐 文档地址
+
+- [arco.design](https://arco.design/vue/docs/start)
+- [字节跳动原版](https://vue-pro.arco.design)

+ 3 - 0
simulation-ui/babel.config.js

@@ -0,0 +1,3 @@
+module.exports = {
+  plugins: ['@vue/babel-plugin-jsx'],
+};

+ 3 - 0
simulation-ui/commitlint.config.js

@@ -0,0 +1,3 @@
+module.exports = {
+  extends: ['@commitlint/config-conventional'],
+};

+ 34 - 0
simulation-ui/config/vite.config.base.ts

@@ -0,0 +1,34 @@
+import { resolve } from 'path';
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import vueJsx from '@vitejs/plugin-vue-jsx';
+import svgLoader from 'vite-svg-loader';
+
+export default defineConfig({
+  base: '/vue-admin-arco/',
+  plugins: [vue(), vueJsx(), svgLoader({ svgoConfig: {} })],
+  resolve: {
+    alias: [
+      {
+        find: '@',
+        replacement: resolve(__dirname, '../src'),
+      },
+      {
+        find: 'assets',
+        replacement: resolve(__dirname, '../src/assets'),
+      },
+      {
+        find: 'vue-i18n',
+        replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue
+      },
+      {
+        find: 'vue',
+        replacement: 'vue/dist/vue.esm-bundler.js', // compile template
+      },
+    ],
+    extensions: ['.ts', '.js'],
+  },
+  define: {
+    'process.env': {},
+  },
+});

+ 23 - 0
simulation-ui/config/vite.config.dev.ts

@@ -0,0 +1,23 @@
+import { mergeConfig } from 'vite';
+import eslint from 'vite-plugin-eslint';
+import baseConig from './vite.config.base';
+
+export default mergeConfig(
+  {
+    mode: 'development',
+    server: {
+      open: true,
+      fs: {
+        strict: true,
+      },
+    },
+    plugins: [
+      eslint({
+        cache: false,
+        include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.vue'],
+        exclude: ['node_modules'],
+      }),
+    ],
+  },
+  baseConig
+);

+ 9 - 0
simulation-ui/config/vite.config.prod.ts

@@ -0,0 +1,9 @@
+import { mergeConfig } from 'vite';
+import baseConfig from './vite.config.base';
+
+export default mergeConfig(
+  {
+    mode: 'production',
+  },
+  { ...baseConfig, base: '/' }
+);

+ 13 - 0
simulation-ui/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="shortcut icon" type="image/x-icon" href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>vue-admin-arco - 开箱即用的中台前端/设计解决方案</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 76 - 0
simulation-ui/package.json

@@ -0,0 +1,76 @@
+{
+  "name": "simulation-ui",
+  "description": "<div align=\"center\">   <h1>vue-admin-arco</h1> </div>",
+  "version": "1.0.1",
+  "private": true,
+  "author": "ArcoDesign Team",
+  "license": "MIT",
+  "scripts": {
+    "dev": "vite --config ./config/vite.config.dev.ts",
+    "build": "vue-tsc --noEmit --skipLibCheck && vite build --config ./config/vite.config.prod.ts",
+    "lint-staged": "npx lint-staged",
+    "prepare": "husky install",
+    "build:normal": "vite build --config ./config/vite.config.prod.ts",
+    "module:update": "ncu -u --registry https://registry.npmmirror.com&&cnpm i",
+    "lint:eslint": "eslint {src,mock}/**/*.{vue,js,ts} --fix"
+  },
+  "dependencies": {
+    "@types/mockjs": "^1.0.6",
+    "@vueuse/core": "^9.2.0",
+    "axios": "^0.27.2",
+    "dayjs": "^1.11.5",
+    "echarts": "^5.3.3",
+    "lodash": "^4.17.21",
+    "mockjs": "^1.1.0",
+    "nprogress": "^0.2.0",
+    "pinia": "^2.0.22",
+    "query-string": "^7.1.1",
+    "vue": "^3.2.39",
+    "vue-echarts": "^6.2.3",
+    "vue-i18n": "^9.2.2",
+    "vue-router": "4"
+  },
+  "devDependencies": {
+    "@arco-design/web-vue": "^2.37.4",
+    "@commitlint/cli": "^17.1.2",
+    "@commitlint/config-conventional": "^17.1.0",
+    "@types/lodash": "^4.14.185",
+    "@types/nprogress": "^0.2.0",
+    "@typescript-eslint/eslint-plugin": "^5.38.0",
+    "@typescript-eslint/parser": "^5.38.0",
+    "@vitejs/plugin-vue": "^3.1.0",
+    "@vitejs/plugin-vue-jsx": "^2.0.1",
+    "@vue/babel-plugin-jsx": "^1.1.1",
+    "eslint": "^8.23.1",
+    "eslint-config-airbnb-base": "^15.0.0",
+    "eslint-config-prettier": "^8.5.0",
+    "eslint-import-resolver-typescript": "^3.5.1",
+    "eslint-plugin-import": "^2.26.0",
+    "eslint-plugin-prettier": "^4.2.1",
+    "eslint-plugin-vue": "^9.5.1",
+    "husky": "^8.0.1",
+    "less": "^4.1.3",
+    "lint-staged": "^13.0.3",
+    "prettier": "^2.7.1",
+    "stylelint": "^14.12.0",
+    "stylelint-config-prettier": "^9.0.3",
+    "stylelint-config-rational-order": "^0.1.2",
+    "stylelint-config-standard": "^28.0.0",
+    "stylelint-order": "^5.0.0",
+    "typescript": "^4.8.3",
+    "vite": "^3.1.3",
+    "vite-plugin-eslint": "^1.8.1",
+    "vite-svg-loader": "^3.6.0",
+    "vue-tsc": "^0.40.13"
+  },
+  "main": ".eslintrc.js",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/chuzhixin/vue-admin-arco.git"
+  },
+  "keywords": [],
+  "bugs": {
+    "url": "https://github.com/chuzhixin/vue-admin-arco/issues"
+  },
+  "homepage": "https://github.com/chuzhixin/vue-admin-arco#readme"
+}

+ 36 - 0
simulation-ui/src/App.vue

@@ -0,0 +1,36 @@
+<template>
+  <a-config-provider :locale="locale">
+    <router-view></router-view>
+    <global-setting />
+  </a-config-provider>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed } from 'vue';
+import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
+import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
+import GlobalSetting from '@/components/global-setting/index.vue';
+import useLocale from '@/hooks/locale';
+
+export default defineComponent({
+  components: {
+    GlobalSetting,
+  },
+  setup() {
+    const { currentLocale } = useLocale();
+    const locale = computed(() => {
+      switch (currentLocale.value) {
+        case 'zh-CN':
+          return zhCN;
+        case 'en-US':
+          return enUS;
+        default:
+          return enUS;
+      }
+    });
+    return {
+      locale,
+    };
+  },
+});
+</script>

+ 21 - 0
simulation-ui/src/api/dashboard.ts

@@ -0,0 +1,21 @@
+import axios from 'axios';
+
+export interface ContentDataRecord {
+  x: string;
+  y: number;
+}
+
+export function queryContentData() {
+  return axios.get<ContentDataRecord[]>('/api/content-data');
+}
+
+export interface PopularRecord {
+  key: number;
+  clickNumber: string;
+  title: string;
+  increases: number;
+}
+
+export function queryPopularList(params: { type: string }) {
+  return axios.get<PopularRecord[]>('/api/popular/list', { params });
+}

+ 21 - 0
simulation-ui/src/api/form.ts

@@ -0,0 +1,21 @@
+import axios from 'axios';
+
+export interface BaseInfoModel {
+  activityName: string;
+  channelType: string;
+  promotionTime: string[];
+  promoteLink: string;
+}
+export interface ChannelInfoModel {
+  advertisingSource: string;
+  advertisingMedia: string;
+  keyword: string[];
+  pushNotify: boolean;
+  advertisingContent: string;
+}
+
+export type UnitChannelModel = BaseInfoModel & ChannelInfoModel;
+
+export function submitChannelForm(data: UnitChannelModel) {
+  return axios.post('/api/channel-form/submit', { data });
+}

+ 60 - 0
simulation-ui/src/api/interceptor.ts

@@ -0,0 +1,60 @@
+import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
+import { Message, Modal } from '@arco-design/web-vue';
+import { useUserStore } from '@/store';
+
+export interface HttpResponse<T = unknown> {
+  status: number;
+  msg: string;
+  code: number;
+  data: T;
+}
+
+axios.interceptors.request.use(
+  (config: AxiosRequestConfig) => {
+    return config;
+  },
+  (error) => {
+    // do something
+    return Promise.reject(error);
+  }
+);
+// add response interceptors
+axios.interceptors.response.use(
+  (response: AxiosResponse<HttpResponse>) => {
+    const res = response.data;
+    // if the custom code is not 20000, it is judged as an error.
+    if (res.code !== 20000) {
+      Message.error({
+        content: res.msg || 'Error',
+        duration: 5 * 1000,
+      });
+      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
+      if (
+        [50008, 50012, 50014].includes(res.code) &&
+        response.config.url !== '/api/user/info'
+      ) {
+        Modal.error({
+          title: 'Confirm logout',
+          content:
+            'You have been logged out, you can cancel to stay on this page, or log in again',
+          okText: 'Re-Login',
+          async onOk() {
+            const userStore = useUserStore();
+
+            await userStore.logout();
+            window.location.reload();
+          },
+        });
+      }
+      return Promise.reject(new Error(res.msg || 'Error'));
+    }
+    return res;
+  },
+  (error) => {
+    Message.error({
+      content: error.msg,
+      duration: 5 * 1000,
+    });
+    return Promise.reject(error);
+  }
+);

+ 56 - 0
simulation-ui/src/api/list.ts

@@ -0,0 +1,56 @@
+import axios from 'axios';
+import qs from 'query-string';
+import { Options } from '@/types/global';
+
+export interface PolicyRecord {
+  id: string;
+  number: number;
+  name: string;
+  contentType: 'img' | 'horizontalVideo' | 'verticalVideo';
+  filterType: 'artificial' | 'rules';
+  count: number;
+  status: 'online' | 'offline';
+  createdTime: string;
+}
+
+export interface PolicyParams extends Partial<PolicyRecord> {
+  current: number;
+  pageSize: number;
+}
+
+export interface PolicyListRes {
+  list: PolicyRecord[];
+  total: number;
+}
+
+export function queryPolicyList(params: PolicyParams) {
+  return axios.get<PolicyListRes>('/api/list/policy', {
+    params,
+    paramsSerializer: (obj) => {
+      return qs.stringify(obj);
+    },
+  });
+}
+
+export interface ServiceRecord {
+  id: number;
+  title: string;
+  description: string;
+  name?: string;
+  actionType?: string;
+  icon?: string;
+  data?: Options[];
+  enable?: boolean;
+  expires?: boolean;
+}
+export function queryInspectionList() {
+  return axios.get('/api/list/quality-inspection');
+}
+
+export function queryTheServiceList() {
+  return axios.get('/api/list/the-service');
+}
+
+export function queryRulesPresetList() {
+  return axios.get('/api/list/rules-preset');
+}

+ 38 - 0
simulation-ui/src/api/message.ts

@@ -0,0 +1,38 @@
+import axios from 'axios';
+
+export interface MessageRecord {
+  id: number;
+  type: string;
+  title: string;
+  subTitle: string;
+  avatar?: string;
+  content: string;
+  time: string;
+  status: 0 | 1;
+  messageType?: number;
+}
+export type MessageListType = MessageRecord[];
+
+export function queryMessageList() {
+  return axios.post<MessageListType>('/api/message/list');
+}
+
+interface MessageStatus {
+  ids: number[];
+}
+
+export function setMessageStatus(data: MessageStatus) {
+  return axios.post<MessageListType>('/api/message/read', data);
+}
+
+export interface ChatRecord {
+  id: number;
+  username: string;
+  content: string;
+  time: string;
+  isCollect: boolean;
+}
+
+export function queryChatList() {
+  return axios.post<ChatRecord[]>('/api/chat/list');
+}

+ 49 - 0
simulation-ui/src/api/profile.ts

@@ -0,0 +1,49 @@
+import axios from 'axios';
+
+export interface ProfileBasicRes {
+  status: number;
+  video: {
+    mode: string;
+    acquisition: {
+      resolution: string;
+      frameRate: number;
+    };
+    encoding: {
+      resolution: string;
+      rate: {
+        min: number;
+        max: number;
+        default: number;
+      };
+      frameRate: number;
+      profile: string;
+    };
+  };
+  audio: {
+    mode: string;
+    acquisition: {
+      channels: number;
+    };
+    encoding: {
+      channels: number;
+      rate: number;
+      profile: string;
+    };
+  };
+}
+
+export function queryProfileBasic() {
+  return axios.get<ProfileBasicRes>('/api/profile/basic');
+}
+
+export type operationLogRes = Array<{
+  key: string;
+  contentNumber: string;
+  updateContent: string;
+  status: number;
+  updateTime: string;
+}>;
+
+export function queryOperationLog() {
+  return axios.get<operationLogRes>('/api/operation/log');
+}

+ 77 - 0
simulation-ui/src/api/user-center.ts

@@ -0,0 +1,77 @@
+import axios from 'axios';
+
+export interface MyProjectRecord {
+  id: number;
+  name: string;
+  description: string;
+  peopleNumber: number;
+  contributors: {
+    name: string;
+    email: string;
+    avatar: string;
+  }[];
+}
+export function queryMyProjectList() {
+  return axios.post('/api/user/my-project/list');
+}
+
+export interface MyTeamRecord {
+  id: number;
+  avatar: string;
+  name: string;
+  peopleNumber: number;
+}
+export function queryMyTeamList() {
+  return axios.post('/api/user/my-team/list');
+}
+
+export interface LatestActivity {
+  id: number;
+  title: string;
+  description: string;
+  avatar: string;
+}
+export function queryLatestActivity() {
+  return axios.post<LatestActivity[]>('/api/user/latest-activity');
+}
+
+export function saveUserInfo() {
+  return axios.post('/api/user/save-info');
+}
+
+export interface BasicInfoModel {
+  email: string;
+  nickname: string;
+  countryRegion: string;
+  area: string;
+  address: string;
+  profile: string;
+}
+
+export interface EnterpriseCertificationModel {
+  accountType: number;
+  status: number;
+  time: string;
+  legalPerson: string;
+  certificateType: string;
+  authenticationNumber: string;
+  enterpriseName: string;
+  enterpriseCertificateType: string;
+  organizationCode: string;
+}
+
+export type CertificationRecord = Array<{
+  certificationType: number;
+  certificationContent: string;
+  status: number;
+  time: string;
+}>;
+
+export interface UnitCertification {
+  enterpriseInfo: EnterpriseCertificationModel;
+  record: CertificationRecord;
+}
+
+export function queryCertification() {
+  return axios.post<UnitCertification>('/api/user/certification');
+}

+ 22 - 0
simulation-ui/src/api/user.ts

@@ -0,0 +1,22 @@
+import axios from 'axios';
+import { UserState } from '@/store/modules/user/types';
+
+export interface LoginData {
+  username: string;
+  password: string;
+}
+
+export interface LoginRes {
+  token: string;
+}
+export function login(data: LoginData) {
+  return axios.post<LoginRes>('/api/user/login', data);
+}
+
+export function logout() {
+  return axios.post<LoginRes>('/api/user/logout');
+}
+
+export function getUserInfo() {
+  return axios.post<UserState>('/api/user/info');
+}

+ 73 - 0
simulation-ui/src/api/visualization.ts

@@ -0,0 +1,73 @@
+import axios from 'axios';
+import { GeneralChart } from '@/types/global';
+
+export interface ChartDataRecord {
+  x: string;
+  y: number;
+  name: string;
+}
+export interface DataChainGrowth {
+  quota: string;
+}
+
+export interface DataChainGrowthRes {
+  count: number;
+  growth: number;
+  chartData: {
+    xAxis: string[];
+    data: { name: string; value: number[] };
+  };
+}
+export function queryDataChainGrowth(data: DataChainGrowth) {
+  return axios.post<DataChainGrowthRes>('/api/data-chain-growth', data);
+}
+
+export interface PopularAuthorRes {
+  list: {
+    ranking: number;
+    author: string;
+    contentCount: number;
+    clickCount: number;
+  }[];
+}
+
+export function queryPopularAuthor() {
+  return axios.get<PopularAuthorRes>('/api/popular-author/list');
+}
+
+export interface ContentPublishRecord {
+  x: string[];
+  y: number[];
+  name: string;
+}
+
+export function queryContentPublish() {
+  return axios.get<ContentPublishRecord[]>('/api/content-publish');
+}
+
+export function queryContentPeriodAnalysis() {
+  return axios.post<GeneralChart>('/api/content-period-analysis');
+}
+
+export interface PublicOpinionAnalysis {
+  quota: string;
+}
+export interface PublicOpinionAnalysisRes {
+  count: number;
+  growth: number;
+  chartData: ChartDataRecord[];
+}
+export function queryPublicOpinionAnalysis(data: DataChainGrowth) {
+  return axios.post<PublicOpinionAnalysisRes>(
+    '/api/public-opinion-analysis',
+    data
+  );
+}
+export interface DataOverviewRes {
+  xAxis: string[];
+  data: Array<{ name: string; value: number[]; count: number }>;
+}
+
+export function queryDataOverview() {
+  return axios.post<DataOverviewRes>('/api/data-overview');
+}

BIN
simulation-ui/src/assets/images/login-banner.png


+ 12 - 0
simulation-ui/src/assets/logo.svg

@@ -0,0 +1,12 @@
+<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M5.37754 16.9795L12.7498 9.43027C14.7163 7.41663 17.9428 7.37837 19.9564 9.34482C19.9852 9.37297 20.0137 9.40145 20.0418 9.43027L20.1221 9.51243C22.1049 11.5429 22.1049 14.7847 20.1221 16.8152L12.7498 24.3644C10.7834 26.378 7.55686 26.4163 5.54322 24.4498C5.5144 24.4217 5.48592 24.3932 5.45777 24.3644L5.37754 24.2822C3.39468 22.2518 3.39468 19.0099 5.37754 16.9795Z" fill="#12D2AC"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0479 9.43034L27.3399 16.8974C29.3674 18.9735 29.3674 22.2883 27.3399 24.3644C25.3735 26.3781 22.147 26.4163 20.1333 24.4499C20.1045 24.4217 20.076 24.3933 20.0479 24.3644L12.7558 16.8974C10.7284 14.8213 10.7284 11.5065 12.7558 9.43034C14.7223 7.4167 17.9488 7.37844 19.9624 9.34489C19.9912 9.37304 20.0197 9.40152 20.0479 9.43034Z" fill="#307AF2"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1321 9.52163L23.6851 13.1599L16.3931 20.627L9.10103 13.1599L12.6541 9.52163C14.6707 7.45664 17.9794 7.4174 20.0444 9.434C20.074 9.46286 20.1032 9.49207 20.1321 9.52163Z" fill="#0057FE"/>
+</g>
+<defs>
+<clipPath id="clip0">
+<rect width="26" height="19" fill="white" transform="translate(3.5 7)"/>
+</clipPath>
+</defs>
+</svg>

+ 44 - 0
simulation-ui/src/assets/style/custom.less

@@ -0,0 +1,44 @@
+body {
+  --color-menu-dark-bg: #000c17;
+  --color-menu-dark-hover: #165dff;
+
+  .arco-menu-dark {
+
+    .arco-menu-item,
+    .arco-menu-group-title,
+    .arco-menu-pop-header,
+    .arco-menu-inline-header {
+      color: #fff;
+
+      * {
+        color: #fff;
+      }
+    }
+
+    .arco-menu-inline-header.arco-menu-selected {
+      color: #fff;
+
+      * {
+        color: #fff;
+      }
+    }
+
+    .arco-menu-inline-header:hover {
+      color: #fff;
+
+      * {
+        color: #fff;
+      }
+    }
+
+    .arco-menu-item:hover {
+      color: #fff;
+    }
+
+    .arco-menu-vertical {
+      .arco-menu-inline-header {
+        line-height: 46px;
+      }
+    }
+  }
+}

+ 104 - 0
simulation-ui/src/assets/style/global.less

@@ -0,0 +1,104 @@
+@import "./custom.less";
+
+* {
+  box-sizing: border-box;
+}
+
+html,
+body {
+  width: 100%;
+  height: 100%;
+  margin: 0;
+  padding: 0;
+  font-size: 14px;
+  background-color: var(--color-bg-1);
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+
+}
+
+.echarts-tooltip-diy {
+  background: linear-gradient(304.17deg,
+      rgb(253 254 255 / 60%) -6.04%,
+      rgb(244 247 252 / 60%) 85.2%) !important;
+  border: none !important;
+
+  /* Note: backdrop-filter has minimal browser support */
+
+  border-radius: 6px !important;
+  backdrop-filter: blur(10px) !important;
+
+  .content-panel {
+    display: flex;
+    justify-content: space-between;
+    width: 164px;
+    height: 32px;
+    margin-bottom: 4px;
+    padding: 0 9px;
+    line-height: 32px;
+    background: rgb(255 255 255 / 80%);
+    border-radius: 4px;
+    box-shadow: 6px 0 20px rgb(34 87 188 / 10%);
+  }
+
+  .tooltip-title {
+    margin: 0 0 10px;
+  }
+
+  p {
+    margin: 0;
+  }
+
+  .tooltip-title,
+  .tooltip-value {
+    display: flex;
+    align-items: center;
+    color: #1d2129;
+    font-weight: bold;
+    font-size: 13px;
+    line-height: 15px;
+    text-align: right;
+  }
+
+  .tooltip-item-icon {
+    display: inline-block;
+    width: 10px;
+    height: 10px;
+    margin-right: 8px;
+    border-radius: 50%;
+  }
+}
+
+.general-card {
+  border: none;
+  border-radius: 4px;
+
+  &>.arco-card-header {
+    height: auto;
+    padding: 20px;
+    border: none;
+  }
+
+  &>.arco-card-body {
+    padding: 0 20px 20px;
+  }
+}
+
+.split-line {
+  border-color: rgb(var(--gray-2));
+}
+
+.arco-table-cell {
+  .circle {
+    display: inline-block;
+    width: 6px;
+    height: 6px;
+    margin-right: 4px;
+    background-color: rgb(var(--blue-6));
+    border-radius: 50%;
+
+    &.pass {
+      background-color: rgb(var(--green-6));
+    }
+  }
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
simulation-ui/src/assets/world.json


+ 37 - 0
simulation-ui/src/components/breadcrumb/index.vue

@@ -0,0 +1,37 @@
+<template>
+  <a-breadcrumb class="container-breadcrumb">
+    <a-breadcrumb-item>
+      <icon-apps />
+    </a-breadcrumb-item>
+    <a-breadcrumb-item v-for="item in items" :key="item">
+      {{ $t(item) }}
+    </a-breadcrumb-item>
+  </a-breadcrumb>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+
+export default defineComponent({
+  props: {
+    items: {
+      type: Array as PropType<string[]>,
+      default() {
+        return [];
+      },
+    },
+  },
+});
+</script>
+
+<style scoped lang="less">
+.container-breadcrumb {
+  margin: 16px 0;
+  :deep(.arco-breadcrumb-item) {
+    color: rgb(var(--gray-6));
+    &:last-child {
+      color: rgb(var(--gray-8));
+    }
+  }
+}
+</style>

+ 58 - 0
simulation-ui/src/components/chart/index.vue

@@ -0,0 +1,58 @@
+<template>
+  <VCharts
+    v-if="renderChart"
+    :option="options"
+    :autoresize="autoresize"
+    :style="{ width, height }"
+  />
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, computed, nextTick } from 'vue';
+import VCharts from 'vue-echarts';
+import { useAppStore } from '@/store';
+
+export default defineComponent({
+  components: {
+    VCharts,
+  },
+  props: {
+    options: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    autoresize: {
+      type: Boolean,
+      default: true,
+    },
+    width: {
+      type: String,
+      default: '100%',
+    },
+    height: {
+      type: String,
+      default: '100%',
+    },
+  },
+  setup() {
+    const appStore = useAppStore();
+    const theme = computed(() => {
+      if (appStore.theme === 'dark') return 'dark';
+      return '';
+    });
+    const renderChart = ref(false);
+    // wait container expand
+    nextTick(() => {
+      renderChart.value = true;
+    });
+    return {
+      theme,
+      renderChart,
+    };
+  },
+});
+</script>
+
+<style scoped lang="less"></style>

+ 31 - 0
simulation-ui/src/components/footer/index.vue

@@ -0,0 +1,31 @@
+<template>
+  <a-layout-footer class="footer">
+    <b> vue-admin-arco </b>
+    &nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;
+    <p
+      >本项目目前绝大多数源码均源自字节跳动 Arco Design
+      开源仓库,仅用于学习用途,由于字节跳动写的实在太香,故没有太多可以改进的地方。
+    </p>
+  </a-layout-footer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({});
+</script>
+
+<style lang="less" scoped>
+.footer {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 60px;
+  color: var(--color-text-2);
+  text-align: center;
+
+  p {
+    font-size: 12px;
+  }
+}
+</style>

+ 79 - 0
simulation-ui/src/components/global-setting/block.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="block">
+    <h5 class="title">{{ title }}</h5>
+    <div v-for="option in options" :key="option.name" class="switch-wrapper">
+      <span>{{ $t(option.name) }}</span>
+      <form-wrapper
+        :type="option.type || 'switch'"
+        :name="option.key"
+        :default-value="option.defaultVal"
+        @input-change="handleChange"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+import { useAppStore } from '@/store';
+import FormWrapper from './form-wrapper.vue';
+
+interface OptionsProps {
+  name: string;
+  key: string;
+  type?: string;
+  defaultVal?: boolean | string | number;
+}
+
+export default defineComponent({
+  components: {
+    FormWrapper,
+  },
+  props: {
+    title: {
+      type: String,
+      default: '',
+    },
+    options: {
+      type: Array as PropType<OptionsProps[]>,
+      default() {
+        return [];
+      },
+    },
+  },
+  setup() {
+    const appStore = useAppStore();
+    const handleChange = ({ key, value }: { key: string; value: unknown }) => {
+      if (value && key === 'colorWeek') {
+        document.body.style.filter = 'invert(80%)';
+      }
+      if (!value && key === 'colorWeek') {
+        document.body.style.filter = 'none';
+      }
+      appStore.updateSettings({ [key]: value });
+    };
+    return {
+      handleChange,
+    };
+  },
+});
+</script>
+
+<style scoped lang="less">
+.block {
+  margin-bottom: 24px;
+}
+
+.title {
+  margin: 10px 0;
+  padding: 0;
+  font-size: 14px;
+}
+
+.switch-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 32px;
+}
+</style>

+ 48 - 0
simulation-ui/src/components/global-setting/form-wrapper.vue

@@ -0,0 +1,48 @@
+<template>
+  <a-input-number
+    v-if="type === 'number'"
+    :style="{ width: '80px' }"
+    size="small"
+    :default-value="defaultValue"
+    @change="handleChange"
+  />
+  <a-switch
+    v-else
+    :default-checked="defaultValue"
+    size="small"
+    @change="handleChange"
+  />
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+  props: {
+    type: {
+      type: String,
+      default: '',
+    },
+    name: {
+      type: String,
+      default: '',
+    },
+    defaultValue: {
+      type: [String, Boolean, Number],
+      default: '',
+    },
+  },
+  emits: ['inputChange'],
+  setup(props, { emit }) {
+    const handleChange = (value: unknown) => {
+      emit('inputChange', {
+        value,
+        key: props.name,
+      });
+    };
+    return {
+      handleChange,
+    };
+  },
+});
+</script>

+ 71 - 0
simulation-ui/src/components/global-setting/index.vue

@@ -0,0 +1,71 @@
+<template>
+  <a-drawer
+    :width="300"
+    unmount-on-close
+    :visible="visible"
+    :cancel-text="$t('settings.close')"
+    :ok-text="$t('settings.copySettings')"
+    @ok="copySettings"
+    @cancel="cancel"
+  >
+    <template #title> {{ $t('settings.title') }} </template>
+    <Block :options="contentOpts" :title="$t('settings.content')" />
+    <Block :options="othersOpts" :title="$t('settings.otherSettings')" />
+    <a-alert>{{ $t('settings.alertContent') }}</a-alert>
+  </a-drawer>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed } from 'vue';
+import { Message } from '@arco-design/web-vue';
+import { useI18n } from 'vue-i18n';
+import { useClipboard } from '@vueuse/core';
+import { useAppStore } from '@/store';
+import Block from './block.vue';
+
+export default defineComponent({
+  components: {
+    Block,
+  },
+  emits: ['cancel'],
+  setup(props, { emit }) {
+    const appStore = useAppStore();
+    const { t } = useI18n();
+    const { copy } = useClipboard();
+    const visible = computed(() => appStore.globalSettings);
+    const contentOpts = [
+      { name: 'settings.navbar', key: 'navbar', defaultVal: true },
+      { name: 'settings.menu', key: 'menu', defaultVal: true },
+      { name: 'settings.footer', key: 'footer', defaultVal: true },
+      /* {
+        name: 'settings.menuWidth',
+        key: 'menuWidth',
+        defaultVal: appStore.menuWidth,
+        type: 'number',
+      }, */
+    ];
+    const othersOpts = [
+      { name: 'settings.colorWeek', key: 'colorWeek', defaultVal: false },
+    ];
+
+    const cancel = () => {
+      appStore.updateSettings({ globalSettings: false });
+      emit('cancel');
+    };
+    const copySettings = async () => {
+      const text = JSON.stringify(appStore.$state, null, 2);
+      await copy(text);
+      Message.success(t('settings.copySettings.message'));
+    };
+    return {
+      visible,
+      contentOpts,
+      othersOpts,
+      copySettings,
+      cancel,
+    };
+  },
+});
+</script>
+
+<style scoped lang="less"></style>

+ 37 - 0
simulation-ui/src/components/index.ts

@@ -0,0 +1,37 @@
+import { App } from 'vue';
+import { use } from 'echarts/core';
+import { CanvasRenderer } from 'echarts/renderers';
+import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
+import {
+  GridComponent,
+  TooltipComponent,
+  LegendComponent,
+  DataZoomComponent,
+  GraphicComponent,
+} from 'echarts/components';
+import Chart from './chart/index.vue';
+import Breadcrumb from './breadcrumb/index.vue';
+// import SvgIcon from './svg-icon/index.vue';
+
+// Manually introduce ECharts modules to reduce packing size
+
+use([
+  CanvasRenderer,
+  BarChart,
+  LineChart,
+  PieChart,
+  RadarChart,
+  GridComponent,
+  TooltipComponent,
+  LegendComponent,
+  DataZoomComponent,
+  GraphicComponent,
+]);
+
+export default {
+  install(Vue: App) {
+    Vue.component('Chart', Chart);
+    Vue.component('Breadcrumb', Breadcrumb);
+    // Vue.component('SvgIcon', SvgIcon);
+  },
+};

+ 166 - 0
simulation-ui/src/components/menu/index.vue

@@ -0,0 +1,166 @@
+<script lang="tsx">
+import { defineComponent, ref, watch, h, compile, computed } from 'vue';
+import { useI18n } from 'vue-i18n';
+import {
+  useRouter,
+  useRoute,
+  RouteRecordRaw,
+  RouteRecordNormalized,
+} from 'vue-router';
+import { useAppStore } from '@/store';
+import usePermission from '@/hooks/permission';
+
+export default defineComponent({
+  emit: ['collapse'],
+  setup() {
+    const { t } = useI18n();
+    const appStore = useAppStore();
+    const permission = usePermission();
+    const router = useRouter();
+    const route = useRoute();
+    const collapsed = ref(false);
+    const appRoute = computed(() => {
+      return router
+        .getRoutes()
+        .find((el) => el.name === 'root') as RouteRecordNormalized;
+    });
+    const menuTree = computed(() => {
+      const copyRouter = JSON.parse(JSON.stringify(appRoute.value.children));
+      function travel(_routes: RouteRecordRaw[], layer: number) {
+        if (!_routes) return null;
+        const collector: any = _routes.map((element) => {
+          // no access
+          if (!permission.accessRouter(element)) {
+            return null;
+          }
+
+          // leaf node
+          if (!element.children) {
+            return element;
+          }
+
+          // route filter hideInMenu true
+          element.children = element.children.filter(
+            (x) => x.meta?.hideInMenu !== true
+          );
+
+          // Associated child node
+          const subItem = travel(element.children, layer);
+          if (subItem.length) {
+            element.children = subItem;
+            return element;
+          }
+          // the else logic
+          if (layer > 1) {
+            element.children = subItem;
+            return element;
+          }
+
+          if (element.meta?.hideInMenu === false) {
+            return element;
+          }
+
+          return null;
+        });
+        return collector.filter(Boolean);
+      }
+      return travel(copyRouter, 0);
+    });
+
+    // In this case only two levels of menus are available
+    // You can expand as needed
+
+    const selectedKey = ref<string[]>([]);
+    const goto = (item: RouteRecordRaw) => {
+      router.push({
+        name: item.name,
+      });
+    };
+    watch(
+      route,
+      (newVal) => {
+        if (newVal.meta.requiresAuth && !newVal.meta.hideInMenu) {
+          const key = newVal.matched[2]?.name as string;
+          selectedKey.value = [key];
+        }
+      },
+      {
+        immediate: true,
+      }
+    );
+    watch(
+      () => appStore.menuCollapse,
+      (newVal) => {
+        collapsed.value = newVal;
+      },
+      {
+        immediate: true,
+      }
+    );
+    const setCollapse = (val: boolean) => {
+      appStore.updateSettings({ menuCollapse: val });
+    };
+
+    const renderSubMenu = () => {
+      function travel(_route: RouteRecordRaw[], nodes = []) {
+        if (_route) {
+          _route.forEach((element) => {
+            // This is demo, modify nodes as needed
+            const icon = element?.meta?.icon ? `<${element?.meta?.icon}/>` : ``;
+            const r = (
+              <a-sub-menu
+                key={element?.name}
+                v-slots={{
+                  icon: () => h(compile(icon)),
+                  title: () => h(compile(t(element?.meta?.locale || ''))),
+                }}
+              >
+                {element?.children?.map((elem) => {
+                  return (
+                    <a-menu-item key={elem.name} onClick={() => goto(elem)}>
+                      {t(elem?.meta?.locale || '')}
+                      {travel(elem.children ?? [])}
+                    </a-menu-item>
+                  );
+                })}
+              </a-sub-menu>
+            );
+            nodes.push(r as never);
+          });
+        }
+        return nodes;
+      }
+      return travel(menuTree.value);
+    };
+    return () => (
+      <a-menu
+        theme="dark"
+        v-model:collapsed={collapsed.value}
+        show-collapse-button
+        auto-open={false}
+        selected-keys={selectedKey.value}
+        auto-open-selected={true}
+        level-indent={34}
+        style="height: 100%"
+        onCollapse={setCollapse}
+      >
+        {renderSubMenu()}
+      </a-menu>
+    );
+  },
+});
+</script>
+
+<style lang="less" scoped>
+:deep(.arco-menu-inner) {
+  .arco-menu-inline-header {
+    display: flex;
+    align-items: center;
+  }
+  .arco-icon {
+    &:not(.arco-icon-down) {
+      font-size: 18px;
+    }
+  }
+}
+</style>

+ 147 - 0
simulation-ui/src/components/message-box/index.vue

@@ -0,0 +1,147 @@
+<template>
+  <a-spin style="display: block" :loading="loading">
+    <a-tabs v-model:activeKey="messageType" type="rounded" destroy-on-hide>
+      <a-tab-pane v-for="item in tabList" :key="item.key">
+        <template #title>
+          <span> {{ item.title }}{{ formatUnreadLength(item.key) }} </span>
+        </template>
+        <a-result v-if="!renderList.length" status="404">
+          <template #subtitle> {{ $t('messageBox.noContent') }} </template>
+        </a-result>
+        <List
+          :render-list="renderList"
+          :unread-count="unreadCount"
+          @item-click="handleItemClick"
+        />
+      </a-tab-pane>
+      <template #extra>
+        <a-button type="text" @click="emptyList">
+          {{ $t('messageBox.tab.button') }}
+        </a-button>
+      </template>
+    </a-tabs>
+  </a-spin>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, reactive, toRefs, computed } from 'vue';
+import { useI18n } from 'vue-i18n';
+import {
+  queryMessageList,
+  setMessageStatus,
+  MessageRecord,
+  MessageListType,
+} from '@/api/message';
+import useLoading from '@/hooks/loading';
+import List from './list.vue';
+
+interface TabItem {
+  key: string;
+  title: string;
+  avatar?: string;
+}
+export default defineComponent({
+  components: {
+    List,
+  },
+  setup() {
+    const { loading, setLoading } = useLoading(true);
+    const messageType = ref('message');
+    const { t } = useI18n();
+    const messageData = reactive<{
+      renderList: MessageRecord[];
+      messageList: MessageRecord[];
+    }>({
+      renderList: [],
+      messageList: [],
+    });
+    const refData = toRefs(messageData);
+    const tabList: TabItem[] = [
+      {
+        key: 'message',
+        title: t('messageBox.tab.title.message'),
+      },
+      {
+        key: 'notice',
+        title: t('messageBox.tab.title.notice'),
+      },
+      {
+        key: 'todo',
+        title: t('messageBox.tab.title.todo'),
+      },
+    ];
+    async function fetchSourceData() {
+      setLoading(true);
+      try {
+        const { data } = await queryMessageList();
+        messageData.messageList = data;
+      } catch (err) {
+        // you can report use errorHandler or other
+      } finally {
+        setLoading(false);
+      }
+    }
+    async function readMessage(data: MessageListType) {
+      const ids = data.map((item) => item.id);
+      await setMessageStatus({ ids });
+      fetchSourceData();
+    }
+    const renderList = computed(() => {
+      return messageData.messageList.filter(
+        (item) => messageType.value === item.type
+      );
+    });
+    const unreadCount = computed(() => {
+      return renderList.value.filter((item) => !item.status).length;
+    });
+    const getUnreadList = (type: string) => {
+      const list = messageData.messageList.filter(
+        (item) => item.type === type && !item.status
+      );
+      return list;
+    };
+    const formatUnreadLength = (type: string) => {
+      const list = getUnreadList(type);
+      return list.length ? `(${list.length})` : ``;
+    };
+    const handleItemClick = (items: MessageListType) => {
+      if (renderList.value.length) readMessage([...items]);
+    };
+    const emptyList = () => {
+      messageData.messageList = [];
+    };
+    fetchSourceData();
+    return {
+      loading,
+      tabList,
+      ...refData,
+      renderList,
+      handleItemClick,
+      formatUnreadLength,
+      unreadCount,
+      messageType,
+      emptyList,
+    };
+  },
+});
+</script>
+
+<style scoped lang="less">
+:deep(.arco-popover-popup-content) {
+  padding: 0;
+}
+
+:deep(.arco-list-item-meta) {
+  align-items: flex-start;
+}
+:deep(.arco-tabs-nav) {
+  padding: 14px 0 12px 16px;
+  border-bottom: 1px solid var(--color-neutral-3);
+}
+:deep(.arco-tabs-content) {
+  padding-top: 0;
+  .arco-result-subtitle {
+    color: rgb(var(--gray-6));
+  }
+}
+</style>

+ 155 - 0
simulation-ui/src/components/message-box/list.vue

@@ -0,0 +1,155 @@
+<template>
+  <a-list :bordered="false">
+    <a-list-item
+      v-for="item in renderList"
+      :key="item.id"
+      action-layout="vertical"
+      :style="{
+        opacity: item.status ? 0.5 : 1,
+      }"
+    >
+      <template #extra>
+        <a-tag v-if="item.messageType === 0" color="gray">未开始</a-tag>
+        <a-tag v-else-if="item.messageType === 1" color="green">已开通</a-tag>
+        <a-tag v-else-if="item.messageType === 2" color="blue">进行中</a-tag>
+        <a-tag v-else-if="item.messageType === 3" color="red">即将到期</a-tag>
+      </template>
+      <div class="item-wrap" @click="onItemClick(item)">
+        <a-list-item-meta>
+          <template v-if="item.avatar" #avatar>
+            <a-avatar shape="circle">
+              <img v-if="item.avatar" :src="item.avatar" />
+              <icon-desktop v-else />
+            </a-avatar>
+          </template>
+          <template #title>
+            <a-space :size="4">
+              <span>{{ item.title }}</span>
+              <a-typography-text type="secondary">
+                {{ item.subTitle }}
+              </a-typography-text>
+            </a-space>
+          </template>
+          <template #description>
+            <div>
+              <a-typography-paragraph
+                :ellipsis="{
+                  rows: 1,
+                }"
+                >{{ item.content }}</a-typography-paragraph
+              >
+              <a-typography-text
+                v-if="item.type === 'message'"
+                class="time-text"
+              >
+                {{ item.time }}
+              </a-typography-text>
+            </div>
+          </template>
+        </a-list-item-meta>
+      </div>
+    </a-list-item>
+    <template #footer>
+      <a-space
+        fill
+        :size="0"
+        :class="{ 'add-border-top': renderList.length < showMax }"
+      >
+        <div class="footer-wrap">
+          <a-link @click="allRead">{{ $t('messageBox.allRead') }}</a-link>
+        </div>
+        <div class="footer-wrap">
+          <a-link>{{ $t('messageBox.viewMore') }}</a-link>
+        </div>
+      </a-space>
+    </template>
+    <div
+      v-if="renderList.length && renderList.length < 3"
+      :style="{ height: (showMax - renderList.length) * 86 + 'px' }"
+    ></div>
+  </a-list>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+import { MessageRecord, MessageListType } from '@/api/message';
+
+export default defineComponent({
+  props: {
+    renderList: {
+      type: Array as PropType<MessageListType>,
+      required: true,
+    },
+    unreadCount: {
+      type: Number,
+      default: 0,
+    },
+  },
+  emits: ['itemClick'],
+  setup(props, context) {
+    const allRead = () => {
+      context.emit('itemClick', [...props.renderList]);
+    };
+
+    const onItemClick = (item: MessageRecord) => {
+      if (!item.status) {
+        context.emit('itemClick', [item]);
+      }
+    };
+    const showMax = 3;
+    return {
+      allRead,
+      onItemClick,
+      showMax,
+    };
+  },
+});
+</script>
+
+<style scoped lang="less">
+:deep(.arco-list) {
+  .arco-list-item {
+    min-height: 86px;
+    border-bottom: 1px solid rgb(var(--gray-3));
+  }
+  .arco-list-item-extra {
+    position: absolute;
+    right: 20px;
+  }
+  .arco-list-item-meta-content {
+    flex: 1;
+  }
+  .item-wrap {
+    cursor: pointer;
+  }
+  .time-text {
+    font-size: 12px;
+    color: rgb(var(--gray-6));
+  }
+  .arco-list-footer {
+    padding: 0;
+    height: 50px;
+    line-height: 50px;
+    // border-top: 1px solid rgb(var(--gray-3));
+    .arco-space-item {
+      width: 100%;
+      border-right: 1px solid rgb(var(--gray-3));
+      &:last-child {
+        border-right: none;
+      }
+    }
+    .add-border-top {
+      border-top: 1px solid rgb(var(--gray-3));
+    }
+  }
+  .footer-wrap {
+    text-align: center;
+  }
+  .arco-typography {
+    margin-bottom: 0;
+  }
+  .add-border {
+    border-top: 1px solid rgb(var(--gray-3));
+  }
+}
+</style>

+ 13 - 0
simulation-ui/src/components/message-box/locale/en-US.ts

@@ -0,0 +1,13 @@
+export default {
+  'messageBox.tab.title.message': 'Message',
+  'messageBox.tab.title.notice': 'Notice',
+  'messageBox.tab.title.todo': 'Todo',
+  'messageBox.tab.button': 'empty',
+  'messageBox.allRead': 'All Read',
+  'messageBox.viewMore': 'View More',
+  'messageBox.noContent': 'No Content',
+  'messageBox.switchRoles': 'Switch Roles',
+  'messageBox.userCenter': 'User Center',
+  'messageBox.userSettings': 'User Settings',
+  'messageBox.logout': 'Logout',
+};

+ 13 - 0
simulation-ui/src/components/message-box/locale/zh-CN.ts

@@ -0,0 +1,13 @@
+export default {
+  'messageBox.tab.title.message': '消息',
+  'messageBox.tab.title.notice': '通知',
+  'messageBox.tab.title.todo': '待办',
+  'messageBox.tab.button': '清空',
+  'messageBox.allRead': '全部已读',
+  'messageBox.viewMore': '查看更多',
+  'messageBox.noContent': '暂无内容',
+  'messageBox.switchRoles': '切换角色',
+  'messageBox.userCenter': '用户中心',
+  'messageBox.userSettings': '用户设置',
+  'messageBox.logout': '登出登录',
+};

+ 309 - 0
simulation-ui/src/components/navbar/index.vue

@@ -0,0 +1,309 @@
+<template>
+  <div class="navbar">
+    <div class="left-side">
+      <!-- <a-space>
+        <img
+          alt="logo"
+          src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
+        />
+        <a-typography-title
+          :style="{ margin: 0, fontSize: '18px' }"
+          :heading="5"
+        >
+          vue-admin-arco
+        </a-typography-title>
+      </a-space> -->
+    </div>
+    <ul class="right-side">
+      <li>
+        <a-tooltip :content="$t('settings.search')">
+          <a-button class="nav-btn" type="outline" :shape="'circle'">
+            <template #icon>
+              <icon-search />
+            </template>
+          </a-button>
+        </a-tooltip>
+      </li>
+      <li>
+        <a-tooltip :content="$t('settings.language')">
+          <a-button
+            class="nav-btn"
+            type="outline"
+            :shape="'circle'"
+            @click="setDropDownVisible"
+          >
+            <template #icon>
+              <icon-language />
+            </template>
+          </a-button>
+        </a-tooltip>
+        <a-dropdown trigger="click" @select="changeLocale">
+          <div ref="triggerBtn" class="trigger-btn"></div>
+          <template #content>
+            <a-doption
+              v-for="item in locales"
+              :key="item.value"
+              :value="item.value"
+            >
+              {{ item.label }}
+            </a-doption>
+          </template>
+        </a-dropdown>
+      </li>
+      <li>
+        <a-tooltip
+          :content="
+            theme === 'light'
+              ? $t('settings.navbar.theme.toDark')
+              : $t('settings.navbar.theme.toLight')
+          "
+        >
+          <a-button
+            class="nav-btn"
+            type="outline"
+            :shape="'circle'"
+            @click="toggleTheme"
+          >
+            <template #icon>
+              <icon-moon-fill v-if="theme === 'dark'" />
+              <icon-sun-fill v-else />
+            </template>
+          </a-button>
+        </a-tooltip>
+      </li>
+      <li>
+        <a-tooltip :content="$t('settings.navbar.alerts')">
+          <div class="message-box-trigger">
+            <a-badge :count="9" dot>
+              <a-button
+                class="nav-btn"
+                type="outline"
+                :shape="'circle'"
+                @click="setPopoverVisible"
+              >
+                <icon-notification />
+              </a-button>
+            </a-badge>
+          </div>
+        </a-tooltip>
+        <a-popover
+          trigger="click"
+          :arrow-style="{ display: 'none' }"
+          :content-style="{ padding: 0, minWidth: '400px' }"
+          content-class="message-popover"
+        >
+          <div ref="refBtn" class="ref-btn"></div>
+          <template #content>
+            <message-box />
+          </template>
+        </a-popover>
+      </li>
+      <li>
+        <a-tooltip :content="$t('settings.title')">
+          <a-button
+            class="nav-btn"
+            type="outline"
+            :shape="'circle'"
+            @click="setVisible"
+          >
+            <template #icon>
+              <icon-settings />
+            </template>
+          </a-button>
+        </a-tooltip>
+      </li>
+      <li>
+        <a-dropdown trigger="click">
+          <a-avatar :size="32" :style="{ marginRight: '8px' }">
+            <img alt="avatar" :src="avatar" />
+          </a-avatar>
+          <template #content>
+            <a-doption>
+              <a-space @click="switchGit">
+                <icon-github />
+                <span> 开源地址 </span>
+              </a-space>
+            </a-doption>
+            <a-doption>
+              <a-space @click="switchRoles">
+                <icon-tag />
+                <span>
+                  {{ $t('messageBox.switchRoles') }}
+                </span>
+              </a-space>
+            </a-doption>
+            <a-doption>
+              <a-space @click="$router.push({ name: 'info' })">
+                <icon-user />
+                <span>
+                  {{ $t('messageBox.userCenter') }}
+                </span>
+              </a-space>
+            </a-doption>
+            <a-doption>
+              <a-space @click="$router.push({ name: 'setting' })">
+                <icon-settings />
+                <span>
+                  {{ $t('messageBox.userSettings') }}
+                </span>
+              </a-space>
+            </a-doption>
+            <a-doption>
+              <a-space @click="handleLogout">
+                <icon-export />
+                <span>
+                  {{ $t('messageBox.logout') }}
+                </span>
+              </a-space>
+            </a-doption>
+          </template>
+        </a-dropdown>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed, ref } from 'vue';
+import { Message } from '@arco-design/web-vue';
+import { useDark, useToggle } from '@vueuse/core';
+import { useAppStore, useUserStore } from '@/store';
+import { LOCALE_OPTIONS } from '@/locale';
+import useLocale from '@/hooks/locale';
+import useUser from '@/hooks/user';
+import MessageBox from '../message-box/index.vue';
+
+export default defineComponent({
+  components: {
+    MessageBox,
+  },
+  setup() {
+    const appStore = useAppStore();
+    const userStore = useUserStore();
+    const { logout } = useUser();
+    const { changeLocale } = useLocale();
+    const locales = [...LOCALE_OPTIONS];
+    const avatar = computed(() => {
+      return userStore.avatar;
+    });
+    const theme = computed(() => {
+      return appStore.theme;
+    });
+    const isDark = useDark({
+      selector: 'body',
+      attribute: 'arco-theme',
+      valueDark: 'dark',
+      valueLight: 'light',
+      storageKey: 'arco-theme',
+      onChanged(dark: boolean) {
+        // overridded default behavior
+        appStore.toggleTheme(dark);
+      },
+    });
+    const toggleTheme = useToggle(isDark);
+    const setVisible = () => {
+      appStore.updateSettings({ globalSettings: true });
+    };
+    const refBtn = ref();
+    const triggerBtn = ref();
+    const setPopoverVisible = () => {
+      const event = new MouseEvent('click', {
+        view: window,
+        bubbles: true,
+        cancelable: true,
+      });
+      refBtn.value.dispatchEvent(event);
+    };
+    const handleLogout = () => {
+      logout();
+    };
+    const setDropDownVisible = () => {
+      const event = new MouseEvent('click', {
+        view: window,
+        bubbles: true,
+        cancelable: true,
+      });
+      triggerBtn.value.dispatchEvent(event);
+    };
+    const switchRoles = async () => {
+      const res = await userStore.switchRoles();
+      Message.success(res as string);
+    };
+    const switchGit = () => {
+      window.open('https://github.com/chuzhixin');
+    };
+    return {
+      locales,
+      theme,
+      avatar,
+      changeLocale,
+      toggleTheme,
+      setVisible,
+      setPopoverVisible,
+      refBtn,
+      triggerBtn,
+      handleLogout,
+      setDropDownVisible,
+      switchRoles,
+      switchGit,
+    };
+  },
+});
+</script>
+
+<style scoped lang="less">
+.navbar {
+  display: flex;
+  justify-content: space-between;
+  height: 100%;
+  background-color: var(--color-bg-2);
+  border-bottom: 1px solid var(--color-border);
+  width: calc(100% - 250px);
+}
+
+.left-side {
+  display: flex;
+  align-items: center;
+  padding-left: 20px;
+}
+
+.right-side {
+  display: flex;
+  padding-right: 20px;
+  list-style: none;
+  :deep(.locale-select) {
+    border-radius: 20px;
+  }
+  li {
+    display: flex;
+    align-items: center;
+    padding: 0 10px;
+  }
+
+  a {
+    color: var(--color-text-1);
+    text-decoration: none;
+  }
+  .nav-btn {
+    border-color: rgb(var(--gray-2));
+    color: rgb(var(--gray-8));
+    font-size: 16px;
+  }
+  .trigger-btn,
+  .ref-btn {
+    position: absolute;
+    bottom: 14px;
+  }
+  .trigger-btn {
+    margin-left: 14px;
+  }
+}
+</style>
+
+<style lang="less">
+.message-popover {
+  .arco-popover-content {
+    margin-top: 0;
+  }
+}
+</style>

+ 11 - 0
simulation-ui/src/config/settings.json

@@ -0,0 +1,11 @@
+{
+  "theme": "light",
+  "colorWeek": false,
+  "navbar": true,
+  "menu": true,
+  "menuCollapse": false,
+  "footer": true,
+  "themeColor": "#165DFF",
+  "menuWidth": 250,
+  "globalSettings": false
+}

+ 8 - 0
simulation-ui/src/directive/index.ts

@@ -0,0 +1,8 @@
+import { App } from 'vue';
+import permission from './permission';
+
+export default {
+  install(Vue: App) {
+    Vue.directive('permission', permission);
+  },
+};

+ 30 - 0
simulation-ui/src/directive/permission/index.ts

@@ -0,0 +1,30 @@
+import { DirectiveBinding } from 'vue';
+import { useUserStore } from '@/store';
+
+function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
+  const { value } = binding;
+  const userStore = useUserStore();
+  const { role } = userStore;
+
+  if (Array.isArray(value)) {
+    if (value.length > 0) {
+      const permissionValues = value;
+
+      const hasPermission = permissionValues.includes(role);
+      if (!hasPermission && el.parentNode) {
+        el.parentNode.removeChild(el);
+      }
+    }
+  } else {
+    throw new Error(`need roles! Like v-permission="['admin','user']"`);
+  }
+}
+
+export default {
+  mounted(el: HTMLElement, binding: DirectiveBinding) {
+    checkPermission(el, binding);
+  },
+  updated(el: HTMLElement, binding: DirectiveBinding) {
+    checkPermission(el, binding);
+  },
+};

+ 8 - 0
simulation-ui/src/env.d.ts

@@ -0,0 +1,8 @@
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import { DefineComponent } from 'vue';
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
+  const component: DefineComponent<{}, {}, any>;
+  export default component;
+}

+ 27 - 0
simulation-ui/src/hooks/chart-option.ts

@@ -0,0 +1,27 @@
+import { computed } from 'vue';
+import { EChartsOption } from 'echarts';
+import { useAppStore } from '@/store';
+
+// for code hints
+// import { SeriesOption } from 'echarts';
+// Because there are so many configuration items, this provides a relatively convenient code hint.
+// When using vue, pay attention to the reactive issues. It is necessary to ensure that corresponding functions can be triggered, Typescript does not report errors, and code writing is convenient.
+interface optionsFn {
+  (isDark: boolean): EChartsOption;
+}
+
+export default function useChartOption(sourceOption: optionsFn) {
+  const appStore = useAppStore();
+  const isDark = computed(() => {
+    return appStore.theme === 'dark';
+  });
+  // echarts support https://echarts.apache.org/zh/theme-builder.html
+  // It's not used here
+  // TODO echarts themes
+  const chartOption = computed<EChartsOption>(() => {
+    return sourceOption(isDark.value);
+  });
+  return {
+    chartOption,
+  };
+}

+ 16 - 0
simulation-ui/src/hooks/loading.ts

@@ -0,0 +1,16 @@
+import { ref } from 'vue';
+
+export default function useLoading(initValue = false) {
+  const loading = ref(initValue);
+  const setLoading = (value: boolean) => {
+    loading.value = value;
+  };
+  const toggle = () => {
+    loading.value = !loading.value;
+  };
+  return {
+    loading,
+    setLoading,
+    toggle,
+  };
+}

+ 19 - 0
simulation-ui/src/hooks/locale.ts

@@ -0,0 +1,19 @@
+import { computed } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { Message } from '@arco-design/web-vue';
+
+export default function useLocale() {
+  const i18 = useI18n();
+  const currentLocale = computed(() => {
+    return i18.locale.value;
+  });
+  const changeLocale = (value: string) => {
+    i18.locale.value = value;
+    localStorage.setItem('arco-locale', value);
+    Message.success(i18.t('navbar.action.locale'));
+  };
+  return {
+    currentLocale,
+    changeLocale,
+  };
+}

+ 33 - 0
simulation-ui/src/hooks/permission.ts

@@ -0,0 +1,33 @@
+import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
+import { useUserStore } from '@/store';
+
+export default function usePermission() {
+  const userStore = useUserStore();
+  return {
+    accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
+      return (
+        !route.meta?.requiresAuth ||
+        !route.meta?.roles ||
+        route.meta?.roles?.includes('*') ||
+        route.meta?.roles?.includes(userStore.role)
+      );
+    },
+    findFirstPermissionRoute(_routers: any, role = 'admin') {
+      const cloneRouters = [..._routers];
+      while (cloneRouters.length) {
+        const firstElement = cloneRouters.shift();
+        if (
+          firstElement?.meta?.roles?.find((el: string[]) => {
+            return el.includes('*') || el.includes(role);
+          })
+        )
+          return { name: firstElement.name };
+        if (firstElement?.children) {
+          cloneRouters.push(...firstElement.children);
+        }
+      }
+      return null;
+    },
+    // You can add any rules you want
+  };
+}

+ 26 - 0
simulation-ui/src/hooks/request.ts

@@ -0,0 +1,26 @@
+import { ref, UnwrapRef } from 'vue';
+import { AxiosResponse } from 'axios';
+import { HttpResponse } from '@/api/interceptor';
+import useLoading from './loading';
+
+// use to fetch list
+// Don't use async function. It doesn't work in async function.
+// Use the bind function to add parameters
+// example: useRequest(api.bind(null, {}))
+
+export default function useRequest<T>(
+  api: () => Promise<AxiosResponse<HttpResponse>>,
+  defaultValue = [] as unknown as T,
+  isLoading = true
+) {
+  const { loading, setLoading } = useLoading(isLoading);
+  const response = ref<T>(defaultValue);
+  api()
+    .then((res) => {
+      response.value = res.data as unknown as UnwrapRef<T>;
+    })
+    .finally(() => {
+      setLoading(false);
+    });
+  return { loading, response };
+}

+ 12 - 0
simulation-ui/src/hooks/themes.ts

@@ -0,0 +1,12 @@
+import { computed } from 'vue';
+import { useAppStore } from '@/store';
+
+export default function useThemes() {
+  const appStore = useAppStore();
+  const isDark = computed(() => {
+    return appStore.theme === 'dark';
+  });
+  return {
+    isDark,
+  };
+}

+ 24 - 0
simulation-ui/src/hooks/user.ts

@@ -0,0 +1,24 @@
+import { useRouter } from 'vue-router';
+import { Message } from '@arco-design/web-vue';
+
+import { useUserStore } from '@/store';
+
+export default function useUser() {
+  const router = useRouter();
+  const userStore = useUserStore();
+  const logout = async (logoutTo?: string) => {
+    await userStore.logout();
+    const currentRoute = router.currentRoute.value;
+    Message.success('登出成功');
+    router.push({
+      name: logoutTo && typeof logoutTo === 'string' ? logoutTo : 'login',
+      query: {
+        ...router.currentRoute.value.query,
+        redirect: currentRoute.name as string,
+      },
+    });
+  };
+  return {
+    logout,
+  };
+}

+ 214 - 0
simulation-ui/src/layout/page-layout.vue

@@ -0,0 +1,214 @@
+<template>
+  <a-layout class="layout">
+    <a-layout>
+      <a-layout>
+        <a-layout-sider
+          v-if="menu"
+          class="layout-sider"
+          :breakpoint="'xl'"
+          :collapsed="collapse"
+          :collapsible="true"
+          :width="menuWidth"
+          :style="{ paddingTop: navbar ? '60px' : '' }"
+          :hide-trigger="true"
+          @collapse="setCollapsed"
+        >
+          <div class="menu-wrapper">
+            <div class="left-side">
+              <a-space>
+                <img
+                  alt="logo"
+                  src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
+                />
+                <a-typography-title
+                  :style="{ margin: 0, fontSize: '18px', color: '#fff' }"
+                  :heading="5"
+                >
+                  vue-admin-arco
+                </a-typography-title>
+              </a-space>
+            </div>
+            <Menu />
+          </div>
+        </a-layout-sider>
+
+        <a-layout class="layout-content" :style="paddingStyle">
+          <div v-if="navbar" class="layout-navbar">
+            <NavBar />
+          </div>
+          <a-layout-content>
+            <router-view />
+          </a-layout-content>
+          <Footer v-if="footer" />
+        </a-layout>
+      </a-layout>
+    </a-layout>
+  </a-layout>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed, watch } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { useAppStore, useUserStore } from '@/store';
+import NavBar from '@/components/navbar/index.vue';
+import Menu from '@/components/menu/index.vue';
+import Footer from '@/components/footer/index.vue';
+import usePermission from '@/hooks/permission';
+
+export default defineComponent({
+  components: {
+    NavBar,
+    Menu,
+    Footer,
+  },
+  setup() {
+    const appStore = useAppStore();
+    const userStore = useUserStore();
+    const router = useRouter();
+    const route = useRoute();
+    const permission = usePermission();
+    const navbarHeight = `60px`;
+    const navbar = computed(() => appStore.navbar);
+    const menu = computed(() => appStore.menu);
+    const footer = computed(() => appStore.footer);
+    const menuWidth = computed(() => {
+      return appStore.menuCollapse ? 48 : appStore.menuWidth;
+    });
+    const collapse = computed(() => {
+      return appStore.menuCollapse;
+    });
+    const paddingStyle = computed(() => {
+      const paddingLeft = menu.value
+        ? { paddingLeft: `${menuWidth.value}px` }
+        : {};
+      const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {};
+      return { ...paddingLeft, ...paddingTop };
+    });
+    const setCollapsed = (val: boolean) => {
+      appStore.updateSettings({ menuCollapse: val });
+    };
+    watch(
+      () => userStore.role,
+      (roleValue) => {
+        if (roleValue && !permission.accessRouter(route))
+          router.push({ name: 'notFound' });
+      }
+    );
+    return {
+      navbar,
+      menu,
+      footer,
+      menuWidth,
+      paddingStyle,
+      collapse,
+      setCollapsed,
+    };
+  },
+});
+</script>
+
+<style scoped lang="less">
+@nav-size-height: 60px;
+@layout-max-width: 1100px;
+
+.layout {
+  width: 100%;
+  height: 100%;
+}
+
+.layout-navbar {
+  transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
+  position: fixed;
+  top: 0;
+  left: 250px;
+  z-index: 100;
+  width: 100%;
+  min-width: @layout-max-width;
+  height: @nav-size-height;
+}
+
+.layout-sider {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 99;
+  height: 100%;
+  padding-top: 0 !important;
+
+  .left-side {
+    display: flex;
+    align-items: center;
+    padding-left: 10px;
+    background: var(--color-menu-dark-bg);
+    height: 60px;
+    transition: none !important;
+  }
+
+  &::after {
+    position: absolute;
+    top: 0;
+    right: -1px;
+    display: block;
+    width: 1px;
+    height: 100%;
+    background-color: var(--color-border);
+    content: '';
+  }
+
+  > :deep(.arco-layout-sider-children) {
+    overflow-y: hidden;
+    top: 0;
+  }
+}
+
+.menu-wrapper {
+  transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
+  height: 100%;
+  overflow: auto;
+  overflow-x: hidden;
+  :deep(.arco-menu) {
+    height: calc(100% - 60px) !important;
+    transition: none;
+    ::-webkit-scrollbar {
+      width: 12px;
+      height: 4px;
+    }
+
+    ::-webkit-scrollbar-thumb {
+      border: 4px solid transparent;
+      background-clip: padding-box;
+      border-radius: 7px;
+      background-color: var(--color-text-4);
+    }
+
+    ::-webkit-scrollbar-thumb:hover {
+      background-color: var(--color-text-3);
+    }
+  }
+}
+
+.layout-content {
+  min-width: @layout-max-width;
+  min-height: 100vh;
+  overflow-y: hidden;
+  background-color: var(--color-fill-2);
+  transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
+}
+
+.arco-layout-sider-collapsed {
+  .left-side {
+    width: 50px;
+    .arco-typography {
+      display: none;
+    }
+  }
+  + .layout-content {
+    .layout-navbar {
+      left: 50px !important;
+      .navbar {
+        width: calc(100% - 50px) !important;
+      }
+    }
+  }
+}
+</style>

+ 63 - 0
simulation-ui/src/locale/en-US.ts

@@ -0,0 +1,63 @@
+import localeMessageBox from '@/components/message-box/locale/en-US';
+import localeLogin from '@/views/login/locale/en-US';
+
+import localeWorkplace from '@/views/dashboard/workplace/locale/en-US';
+/** simple */
+import localeMonitor from '@/views/dashboard/monitor/locale/en-US';
+
+import localeSearchTable from '@/views/list/search-table/locale/en-US';
+import localeCardList from '@/views/list/card/locale/en-US';
+
+import localeStepForm from '@/views/form/step/locale/en-US';
+import localeGroupForm from '@/views/form/group/locale/en-US';
+
+import localeBasicProfile from '@/views/profile/basic/locale/en-US';
+
+import localeDataAnalysis from '@/views/visualization/data-analysis/locale/en-US';
+import localeMultiDAnalysis from '@/views/visualization/multi-dimension-data-analysis/locale/en-US';
+
+import localeSuccess from '@/views/result/success/locale/en-US';
+import localeError from '@/views/result/error/locale/en-US';
+
+import locale403 from '@/views/exception/403/locale/en-US';
+import locale404 from '@/views/exception/404/locale/en-US';
+import locale500 from '@/views/exception/500/locale/en-US';
+
+import localeUserInfo from '@/views/user/info/locale/en-US';
+import localeUserSetting from '@/views/user/setting/locale/en-US';
+/** simple end */
+import localeSettings from './en-US/settings';
+
+export default {
+  'menu.dashboard': 'Dashboard',
+  'menu.list': 'List',
+  'menu.result': 'Result',
+  'menu.exception': 'Exception',
+  'menu.form': 'Form',
+  'menu.profile': 'Profile',
+  'menu.visualization': 'Data Visualization',
+  'menu.user': 'User Center',
+  'navbar.docs': 'Docs',
+  'navbar.action.locale': 'Switch to English',
+  ...localeSettings,
+  ...localeMessageBox,
+  ...localeLogin,
+  ...localeWorkplace,
+  /** simple */
+  ...localeMonitor,
+  ...localeSearchTable,
+  ...localeCardList,
+  ...localeStepForm,
+  ...localeGroupForm,
+  ...localeBasicProfile,
+  ...localeDataAnalysis,
+  ...localeMultiDAnalysis,
+  ...localeSuccess,
+  ...localeError,
+  ...locale403,
+  ...locale404,
+  ...locale500,
+  ...localeUserInfo,
+  ...localeUserSetting,
+  /** simple end */
+};

+ 24 - 0
simulation-ui/src/locale/en-US/settings.ts

@@ -0,0 +1,24 @@
+export default {
+  'settings.title': 'Settings',
+  'settings.themeColor': 'Theme Color',
+  'settings.content': 'Content Setting',
+  'settings.search': 'Search',
+  'settings.language': 'Language',
+  'settings.navbar': 'Navbar',
+  'settings.menuWidth': 'Menu Width (px)',
+  'settings.navbar.theme.toLight': 'Click to use light mode',
+  'settings.navbar.theme.toDark': 'Click to use dark mode',
+  'settings.navbar.alerts': 'alerts',
+  'settings.menu': 'Menu',
+  'settings.footer': 'Footer',
+  'settings.otherSettings': 'Other Settings',
+  'settings.colorWeek': 'Color Week',
+  'settings.alertContent':
+    'After the configuration is only temporarily effective, if you want to really affect the project, click the "Copy Settings" button below and replace the configuration in settings.json.',
+  'settings.copySettings': 'Copy Settings',
+  'settings.copySettings.message':
+    'Copy succeeded, please paste to file src/settings.json.',
+  'settings.close': 'Close',
+  'settings.color.tooltip':
+    '10 gradient colors generated according to the theme color',
+};

+ 21 - 0
simulation-ui/src/locale/index.ts

@@ -0,0 +1,21 @@
+import { createI18n } from 'vue-i18n';
+import en from './en-US';
+import cn from './zh-CN';
+
+export const LOCALE_OPTIONS = [
+  { label: '中文', value: 'zh-CN' },
+  { label: 'English', value: 'en-US' },
+];
+const defaultLocale = localStorage.getItem('arco-locale') || 'zh-CN';
+
+const i18n = createI18n({
+  legacy: false,
+  locale: defaultLocale,
+  fallbackLocale: 'en-US',
+  messages: {
+    'en-US': en,
+    'zh-CN': cn,
+  },
+});
+
+export default i18n;

+ 63 - 0
simulation-ui/src/locale/zh-CN.ts

@@ -0,0 +1,63 @@
+import localeMessageBox from '@/components/message-box/locale/zh-CN';
+import localeLogin from '@/views/login/locale/zh-CN';
+
+import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN';
+/** simple */
+import localeMonitor from '@/views/dashboard/monitor/locale/zh-CN';
+
+import localeSearchTable from '@/views/list/search-table/locale/zh-CN';
+import localeCardList from '@/views/list/card/locale/zh-CN';
+
+import localeStepForm from '@/views/form/step/locale/zh-CN';
+import localeGroupForm from '@/views/form/group/locale/zh-CN';
+
+import localeBasicProfile from '@/views/profile/basic/locale/zh-CN';
+
+import localeDataAnalysis from '@/views/visualization/data-analysis/locale/zh-CN';
+import localeMultiDAnalysis from '@/views/visualization/multi-dimension-data-analysis/locale/zh-CN';
+
+import localeSuccess from '@/views/result/success/locale/zh-CN';
+import localeError from '@/views/result/error/locale/zh-CN';
+
+import locale403 from '@/views/exception/403/locale/zh-CN';
+import locale404 from '@/views/exception/404/locale/zh-CN';
+import locale500 from '@/views/exception/500/locale/zh-CN';
+
+import localeUserInfo from '@/views/user/info/locale/zh-CN';
+import localeUserSetting from '@/views/user/setting/locale/zh-CN';
+/** simple end */
+import localeSettings from './zh-CN/settings';
+
+export default {
+  'menu.dashboard': '仪表盘',
+  'menu.list': '列表页',
+  'menu.result': '结果页',
+  'menu.exception': '异常页',
+  'menu.form': '表单页',
+  'menu.profile': '详情页',
+  'menu.visualization': '数据可视化',
+  'menu.user': '个人中心',
+  'navbar.docs': '文档中心',
+  'navbar.action.locale': '切换为中文',
+  ...localeSettings,
+  ...localeMessageBox,
+  ...localeLogin,
+  ...localeWorkplace,
+  /** simple */
+  ...localeMonitor,
+  ...localeSearchTable,
+  ...localeCardList,
+  ...localeStepForm,
+  ...localeGroupForm,
+  ...localeBasicProfile,
+  ...localeDataAnalysis,
+  ...localeMultiDAnalysis,
+  ...localeSuccess,
+  ...localeError,
+  ...locale403,
+  ...locale404,
+  ...locale500,
+  ...localeUserInfo,
+  ...localeUserSetting,
+  /** simple end */
+};

+ 24 - 0
simulation-ui/src/locale/zh-CN/settings.ts

@@ -0,0 +1,24 @@
+export default {
+  'settings.title': '页面配置',
+  'settings.themeColor': '主题色',
+  'settings.content': '内容区域',
+  'settings.search': '搜索',
+  'settings.language': '语言',
+  'settings.navbar': '导航栏',
+  'settings.menuWidth': '菜单宽度 (px)',
+  'settings.navbar.theme.toLight': '点击切换为亮色模式',
+  'settings.navbar.theme.toDark': '点击切换为暗黑模式',
+  'settings.navbar.alerts': '消息通知',
+  'settings.menu': '菜单栏',
+  'settings.footer': '底部',
+  'settings.otherSettings': '其他设置',
+  'settings.colorWeek': '色弱模式',
+  'settings.alertContent':
+    '配置之后仅是临时生效,要想真正作用于项目,点击下方的 "复制配置" 按钮,将配置替换到 settings.json 中即可。',
+  'settings.copySettings': '复制配置',
+  'settings.copySettings.message':
+    '复制成功,请粘贴到 src/settings.json 文件中',
+  'settings.close': '关闭',
+  'settings.color.tooltip':
+    '根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)',
+};

+ 26 - 0
simulation-ui/src/main.ts

@@ -0,0 +1,26 @@
+import { createApp } from 'vue';
+import ArcoVue from '@arco-design/web-vue';
+import ArcoVueIcon from '@arco-design/web-vue/es/icon';
+import globalComponents from '@/components';
+import router from './router';
+import store from './store';
+import i18n from './locale';
+import directive from './directive';
+import './mock';
+import App from './App.vue';
+import '@arco-design/web-vue/dist/arco.css';
+import '@/assets/style/global.less';
+import '@/api/interceptor';
+
+const app = createApp(App);
+
+app.use(ArcoVue, {});
+app.use(ArcoVueIcon);
+
+app.use(router);
+app.use(store);
+app.use(i18n);
+app.use(globalComponents);
+app.use(directive);
+
+app.mount('#app');

+ 26 - 0
simulation-ui/src/mock/index.ts

@@ -0,0 +1,26 @@
+import Mock from 'mockjs';
+
+import './user';
+import './message-box';
+
+import '@/views/dashboard/workplace/mock';
+/** simple */
+import '@/views/dashboard/monitor/mock';
+
+import '@/views/list/card/mock';
+import '@/views/list/search-table/mock';
+
+import '@/views/form/step/mock';
+
+import '@/views/profile/basic/mock';
+
+import '@/views/visualization/data-analysis/mock';
+import '@/views/visualization/multi-dimension-data-analysis/mock';
+
+import '@/views/user/info/mock';
+import '@/views/user/setting/mock';
+/** simple end */
+
+Mock.setup({
+  timeout: '600-1000',
+});

+ 85 - 0
simulation-ui/src/mock/message-box.ts

@@ -0,0 +1,85 @@
+import Mock from 'mockjs';
+import setupMock, { successResponseWrap } from '@/utils/setup-mock';
+
+const haveReadIds: number[] = [];
+const getMessageList = () => {
+  return [
+    {
+      id: 1,
+      type: 'message',
+      title: '郑曦月',
+      subTitle: '的私信',
+      avatar:
+        '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/8361eeb82904210b4f55fab888fe8416.png~tplv-uwbnlip3yd-webp.webp',
+      content: '审批请求已发送,请查收',
+      time: '今天 12:30:01',
+    },
+    {
+      id: 2,
+      type: 'message',
+      title: '宁波',
+      subTitle: '的回复',
+      avatar:
+        '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
+      content: '此处 bug 已经修复',
+      time: '今天 12:30:01',
+    },
+    {
+      id: 3,
+      type: 'message',
+      title: '宁波',
+      subTitle: '的回复',
+      avatar:
+        '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
+      content: '此处 bug 已经修复',
+      time: '今天 12:20:01',
+    },
+    {
+      id: 4,
+      type: 'notice',
+      title: '续费通知',
+      subTitle: '',
+      avatar: '',
+      content: '您的产品使用期限即将截止,如需继续使用产品请前往购…',
+      time: '今天 12:20:01',
+      messageType: 3,
+    },
+    {
+      id: 5,
+      type: 'notice',
+      title: '规则开通成功',
+      subTitle: '',
+      avatar: '',
+      content: '内容屏蔽规则于 2021-12-01 开通成功并生效',
+      time: '今天 12:20:01',
+      messageType: 1,
+    },
+    {
+      id: 6,
+      type: 'todo',
+      title: '质检队列变更',
+      subTitle: '',
+      avatar: '',
+      content: '内容质检队列于 2021-12-01 19:50:23 进行变更,请重新…',
+      time: '今天 12:20:01',
+      messageType: 0,
+    },
+  ].map((item) => ({
+    ...item,
+    status: haveReadIds.indexOf(item.id) === -1 ? 0 : 1,
+  }));
+};
+
+setupMock({
+  setup: () => {
+    Mock.mock(new RegExp('/api/message/list'), () => {
+      return successResponseWrap(getMessageList());
+    });
+
+    Mock.mock(new RegExp('/api/message/read'), (params: { body: string }) => {
+      const { ids } = JSON.parse(params.body);
+      haveReadIds.push(...(ids || []));
+      return successResponseWrap(true);
+    });
+  },
+});

+ 69 - 0
simulation-ui/src/mock/user.ts

@@ -0,0 +1,69 @@
+import Mock from 'mockjs';
+import setupMock, {
+  successResponseWrap,
+  failResponseWrap,
+} from '@/utils/setup-mock';
+
+import { MockParams } from '@/types/mock';
+import { isLogin } from '@/utils/auth';
+
+setupMock({
+  setup() {
+    // Mock.XHR.prototype.withCredentials = true;
+
+    // 用户信息
+    Mock.mock(new RegExp('/api/user/info'), () => {
+      if (isLogin()) {
+        const role = window.localStorage.getItem('userRole') || 'admin';
+        return successResponseWrap({
+          name: '王立群',
+          avatar:
+            '//lf1-xgcdn-tos.pstatp.com/obj/vcloud/vadmin/start.8e0e4855ee346a46ccff8ff3e24db27b.png',
+          email: 'wangliqun@email.com',
+          job: 'frontend',
+          jobName: '前端艺术家',
+          organization: 'Frontend',
+          organizationName: '前端',
+          location: 'beijing',
+          locationName: '北京',
+          introduction: '人潇洒,性温存',
+          personalWebsite: 'https://www.arco.design',
+          phone: '150****0000',
+          registrationDate: '2013-05-10 12:10:00',
+          accountId: '15012312300',
+          certification: 1,
+          role,
+        });
+      }
+      return failResponseWrap(null, '未登录', 50008);
+    });
+
+    // 登录
+    Mock.mock(new RegExp('/api/user/login'), (params: MockParams) => {
+      const { username, password } = JSON.parse(params.body);
+      if (!username) {
+        return failResponseWrap(null, '用户名不能为空', 50000);
+      }
+      if (!password) {
+        return failResponseWrap(null, '密码不能为空', 50000);
+      }
+      if (username === 'admin' && password === 'admin') {
+        window.localStorage.setItem('userRole', 'admin');
+        return successResponseWrap({
+          token: '12345',
+        });
+      }
+      if (username === 'user' && password === 'user') {
+        window.localStorage.setItem('userRole', 'user');
+        return successResponseWrap({
+          token: '54321',
+        });
+      }
+      return failResponseWrap(null, '账号或者密码错误', 50000);
+    });
+    // 登出
+    Mock.mock(new RegExp('/api/user/logout'), () => {
+      return successResponseWrap(null);
+    });
+  },
+});

+ 95 - 0
simulation-ui/src/router/index.ts

@@ -0,0 +1,95 @@
+import {
+  createRouter,
+  createWebHashHistory,
+  LocationQueryRaw,
+} from 'vue-router';
+import NProgress from 'nprogress'; // progress bar
+import 'nprogress/nprogress.css';
+
+import usePermission from '@/hooks/permission';
+import { useUserStore } from '@/store';
+import PageLayout from '@/layout/page-layout.vue';
+import { isLogin } from '@/utils/auth';
+import Login from './modules/login';
+import appRoutes from './modules';
+
+NProgress.configure({ showSpinner: false }); // NProgress Configuration
+
+const router = createRouter({
+  history: createWebHashHistory(''),
+  routes: [
+    {
+      path: '/',
+      redirect: 'login',
+    },
+    Login,
+    {
+      name: 'root',
+      path: '/',
+      component: PageLayout,
+      children: appRoutes,
+    },
+    {
+      path: '/:pathMatch(.*)*',
+      name: 'notFound',
+      component: () => import('@/views/not-found/index.vue'),
+    },
+  ],
+  scrollBehavior() {
+    return { top: 0 };
+  },
+});
+
+router.beforeEach(async (to, from, next) => {
+  NProgress.start();
+  const userStore = useUserStore();
+  async function crossroads() {
+    const Permission = usePermission();
+    if (Permission.accessRouter(to)) await next();
+    else {
+      const destination = Permission.findFirstPermissionRoute(
+        appRoutes,
+        userStore.role
+      ) || {
+        name: 'notFound',
+      };
+      await next(destination);
+    }
+    NProgress.done();
+  }
+  if (isLogin()) {
+    if (userStore.role) {
+      crossroads();
+    } else {
+      try {
+        await userStore.info();
+        crossroads();
+      } catch (error) {
+        next({
+          name: 'login',
+          query: {
+            redirect: to.name,
+            ...to.query,
+          } as LocationQueryRaw,
+        });
+        NProgress.done();
+      }
+    }
+  } else {
+    if (to.name === 'login') {
+      next();
+      NProgress.done();
+      return;
+    }
+    next({
+      name: 'login',
+      query: {
+        redirect: to.name,
+        ...to.query,
+      } as LocationQueryRaw,
+    });
+    NProgress.done();
+  }
+});
+
+export default router;

+ 34 - 0
simulation-ui/src/router/modules/dashboard.ts

@@ -0,0 +1,34 @@
+export default {
+  path: 'dashboard',
+  name: 'dashboard',
+  component: () => import('@/views/dashboard/index.vue'),
+  meta: {
+    locale: 'menu.dashboard',
+    requiresAuth: true,
+    icon: 'icon-dashboard',
+  },
+  children: [
+    {
+      path: 'workplace',
+      name: 'workplace',
+      component: () => import('@/views/dashboard/workplace/index.vue'),
+      meta: {
+        locale: 'menu.dashboard.workplace',
+        requiresAuth: true,
+        roles: ['*'],
+      },
+    },
+    /** simple */
+    {
+      path: 'monitor',
+      name: 'monitor',
+      component: () => import('@/views/dashboard/monitor/index.vue'),
+      meta: {
+        locale: 'menu.dashboard.monitor',
+        requiresAuth: true,
+        roles: ['admin'],
+      },
+    },
+    /** simple end */
+  ],
+};

+ 42 - 0
simulation-ui/src/router/modules/exception.ts

@@ -0,0 +1,42 @@
+export default {
+  path: 'exception',
+  name: 'exception',
+  component: () => import('@/views/exception/index.vue'),
+  meta: {
+    locale: 'menu.exception',
+    requiresAuth: true,
+    icon: 'icon-exclamation-circle',
+  },
+  children: [
+    {
+      path: '403',
+      name: '403',
+      component: () => import('@/views/exception/403/index.vue'),
+      meta: {
+        locale: 'menu.exception.403',
+        requiresAuth: true,
+        roles: ['admin'],
+      },
+    },
+    {
+      path: '404',
+      name: '404',
+      component: () => import('@/views/exception/404/index.vue'),
+      meta: {
+        locale: 'menu.exception.404',
+        requiresAuth: true,
+        roles: ['*'],
+      },
+    },
+    {
+      path: '500',
+      name: '500',
+      component: () => import('@/views/exception/500/index.vue'),
+      meta: {
+        locale: 'menu.exception.500',
+        requiresAuth: true,
+        roles: ['*'],
+      },
+    },
+  ],
+};

+ 32 - 0
simulation-ui/src/router/modules/form.ts

@@ -0,0 +1,32 @@
+export default {
+  path: 'form',
+  name: 'form',
+  component: () => import('@/views/form/index.vue'),
+  meta: {
+    locale: 'menu.form',
+    icon: 'icon-settings',
+    requiresAuth: true,
+  },
+  children: [
+    {
+      path: 'step',
+      name: 'step',
+      component: () => import('@/views/form/step/index.vue'),
+      meta: {
+        locale: 'menu.form.step',
+        requiresAuth: true,
+        roles: ['admin'],
+      },
+    },
+    {
+      path: 'group',
+      name: 'group',
+      component: () => import('@/views/form/group/index.vue'),
+      meta: {
+        locale: 'menu.form.group',
+        requiresAuth: true,
+        roles: ['admin'],
+      },
+    },
+  ],
+};

+ 23 - 0
simulation-ui/src/router/modules/index.ts

@@ -0,0 +1,23 @@
+import Dashboard from './dashboard';
+/** simple */
+import List from './list';
+import Form from './form';
+import Profile from './profile';
+import Visualization from './visualization';
+import Result from './result';
+import Exception from './exception';
+import User from './user';
+/** simple end */
+
+export default [
+  Dashboard,
+  /** simple */
+  Visualization,
+  List,
+  Form,
+  Profile,
+  Result,
+  Exception,
+  User,
+  /** simple end */
+];

+ 32 - 0
simulation-ui/src/router/modules/list.ts

@@ -0,0 +1,32 @@
+export default {
+  path: 'list',
+  name: 'list',
+  component: () => import('@/views/list/index.vue'),
+  meta: {
+    locale: 'menu.list',
+    requiresAuth: true,
+    icon: 'icon-list',
+  },
+  children: [
+    {
+      path: 'search-table', // The midline path complies with SEO specifications
+      name: 'searchTable',
+      component: () => import('@/views/list/search-table/index.vue'),
+      meta: {
+        locale: 'menu.list.searchTable',
+        requiresAuth: true,
+        roles: ['*'],
+      },
+    },
+    {
+      path: 'card',
+      name: 'card',
+      component: () => import('@/views/list/card/index.vue'),
+      meta: {
+        locale: 'menu.list.cardList',
+        requiresAuth: true,
+        roles: ['*'],
+      },
+    },
+  ],
+};

+ 9 - 0
simulation-ui/src/router/modules/login.ts

@@ -0,0 +1,9 @@
+export default {
+  path: '/login',
+  name: 'login',
+  component: () => import('@/views/login/index.vue'),
+  meta: {
+    title: '',
+    requiresAuth: false,
+  },
+};

+ 22 - 0
simulation-ui/src/router/modules/profile.ts

@@ -0,0 +1,22 @@
+export default {
+  path: 'profile',
+  name: 'profile',
+  component: () => import('@/views/profile/index.vue'),
+  meta: {
+    locale: 'menu.profile',
+    requiresAuth: true,
+    icon: 'icon-file',
+  },
+  children: [
+    {
+      path: 'basic',
+      name: 'basic',
+      component: () => import('@/views/profile/basic/index.vue'),
+      meta: {
+        locale: 'menu.profile.basic',
+        requiresAuth: true,
+        roles: ['admin'],
+      },
+    },
+  ],
+};

+ 32 - 0
simulation-ui/src/router/modules/result.ts

@@ -0,0 +1,32 @@
+export default {
+  path: 'result',
+  name: 'result',
+  component: () => import('@/views/result/index.vue'),
+  meta: {
+    locale: 'menu.result',
+    icon: 'icon-check-circle',
+    requiresAuth: true,
+  },
+  children: [
+    {
+      path: 'success',
+      name: 'success',
+      component: () => import('@/views/result/success/index.vue'),
+      meta: {
+        locale: 'menu.result.success',
+        requiresAuth: true,
+        roles: ['admin'],
+      },
+    },
+    {
+      path: 'error',
+      name: 'error',
+      component: () => import('@/views/result/error/index.vue'),
+      meta: {
+        locale: 'menu.result.error',
+        requiresAuth: true,
+        roles: ['admin'],
+      },
+    },
+  ],
+};

+ 32 - 0
simulation-ui/src/router/modules/user.ts

@@ -0,0 +1,32 @@
+export default {
+  path: 'user',
+  name: 'user',
+  component: () => import('@/views/user/index.vue'),
+  meta: {
+    locale: 'menu.user',
+    icon: 'icon-user',
+    requiresAuth: true,
+  },
+  children: [
+    {
+      path: 'info',
+      name: 'info',
+      component: () => import('@/views/user/info/index.vue'),
+      meta: {
+        locale: 'menu.user.info',
+        requiresAuth: true,
+        roles: ['*'],
+      },
+    },
+    {
+      path: 'setting',
+      name: 'setting',
+      component: () => import('@/views/user/setting/index.vue'),
+      meta: {
+        locale: 'menu.user.setting',
+        requiresAuth: true,
+        roles: ['*'],
+      },
+    },
+  ],
+};

+ 33 - 0
simulation-ui/src/router/modules/visualization.ts

@@ -0,0 +1,33 @@
+export default {
+  path: 'visualization',
+  name: 'visualization',
+  component: () => import('@/views/visualization/index.vue'),
+  meta: {
+    locale: 'menu.visualization',
+    requiresAuth: true,
+    icon: 'icon-apps',
+  },
+  children: [
+    {
+      path: 'data-analysis',
+      name: 'dataAnalysis',
+      component: () => import('@/views/visualization/data-analysis/index.vue'),
+      meta: {
+        locale: 'menu.visualization.dataAnalysis',
+        requiresAuth: true,
+        roles: ['admin'],
+      },
+    },
+    {
+      path: 'multi-dimension-data-analysis',
+      name: 'multiDimensionDataAnalysis',
+      component: () =>
+        import('@/views/visualization/multi-dimension-data-analysis/index.vue'),
+      meta: {
+        locale: 'menu.visualization.multiDimensionDataAnalysis',
+        requiresAuth: true,
+        roles: ['admin'],
+      },
+    },
+  ],
+};

+ 15 - 0
simulation-ui/src/router/typings.d.ts

@@ -0,0 +1,15 @@
+import 'vue-router';
+
+declare module 'vue-router' {
+  interface RouteMeta {
+    // options
+    roles?: string[];
+    // every route must declare
+    requiresAuth: boolean; // need login
+    icon?: string;
+    locale?: string;
+    // menu select key
+    menuSelectKey?: string;
+    hideInMenu?: boolean;
+  }
+}

+ 8 - 0
simulation-ui/src/store/index.ts

@@ -0,0 +1,8 @@
+import { createPinia } from 'pinia';
+import useAppStore from './modules/app';
+import useUserStore from './modules/user';
+
+const pinia = createPinia();
+
+export { useAppStore, useUserStore };
+export default pinia;

+ 34 - 0
simulation-ui/src/store/modules/app/index.ts

@@ -0,0 +1,34 @@
+import { defineStore } from 'pinia';
+import defaultSettings from '@/config/settings.json';
+import { AppState } from './types';
+
+const useAppStore = defineStore('app', {
+  state: (): AppState => ({ ...defaultSettings }),
+
+  getters: {
+    appCurrentSetting(state: AppState): AppState {
+      return { ...state };
+    },
+  },
+
+  actions: {
+    // Update app settings
+    updateSettings(partial: Partial<AppState>) {
+      // @ts-ignore-next-line
+      this.$patch(partial);
+    },
+
+    // Change theme color
+    toggleTheme(dark: boolean) {
+      if (dark) {
+        this.theme = 'dark';
+        document.body.setAttribute('arco-theme', 'dark');
+      } else {
+        this.theme = 'light';
+        document.body.removeAttribute('arco-theme');
+      }
+    },
+  },
+});
+
+export default useAppStore;

+ 12 - 0
simulation-ui/src/store/modules/app/types.ts

@@ -0,0 +1,12 @@
+export interface AppState {
+  theme: string;
+  colorWeek: boolean;
+  navbar: boolean;
+  menu: boolean;
+  menuCollapse: boolean;
+  footer: boolean;
+  themeColor: string;
+  menuWidth: number;
+  globalSettings: boolean;
+  [key: string]: unknown;
+}

+ 82 - 0
simulation-ui/src/store/modules/user/index.ts

@@ -0,0 +1,82 @@
+import { defineStore } from 'pinia';
+import {
+  login as userLogin,
+  logout as userLogout,
+  getUserInfo,
+  LoginData,
+} from '@/api/user';
+import { setToken, clearToken } from '@/utils/auth';
+import { UserState } from './types';
+
+const useUserStore = defineStore('user', {
+  state: (): UserState => ({
+    name: undefined,
+    avatar: undefined,
+    job: undefined,
+    organization: undefined,
+    location: undefined,
+    email: undefined,
+    introduction: undefined,
+    personalWebsite: undefined,
+    jobName: undefined,
+    organizationName: undefined,
+    locationName: undefined,
+    phone: undefined,
+    registrationDate: undefined,
+    accountId: undefined,
+    certification: undefined,
+    role: '',
+  }),
+
+  getters: {
+    userInfo(state: UserState): UserState {
+      return { ...state };
+    },
+  },
+
+  actions: {
+    switchRoles() {
+      return new Promise((resolve) => {
+        this.role = this.role === 'user' ? 'admin' : 'user';
+        resolve(this.role);
+      });
+    },
+    // Set user's information
+    setInfo(partial: Partial<UserState>) {
+      this.$patch(partial);
+    },
+
+    // Reset user's information
+    resetInfo() {
+      this.$reset();
+    },
+
+    // Get user's information
+    async info() {
+      const res = await getUserInfo();
+
+      this.setInfo(res.data);
+    },
+
+    // Login
+    async login(loginForm: LoginData) {
+      try {
+        const res = await userLogin(loginForm);
+        setToken(res.data.token);
+      } catch (err) {
+        clearToken();
+        throw err;
+      }
+    },
+
+    // Logout
+    async logout() {
+      await userLogout();
+
+      this.resetInfo();
+      clearToken();
+    },
+  },
+});
+
+export default useUserStore;

+ 19 - 0
simulation-ui/src/store/modules/user/types.ts

@@ -0,0 +1,19 @@
+export type RoleType = '' | '*' | 'admin' | 'user';
+export interface UserState {
+  name?: string;
+  avatar?: string;
+  job?: string;
+  organization?: string;
+  location?: string;
+  email?: string;
+  introduction?: string;
+  personalWebsite?: string;
+  jobName?: string;
+  organizationName?: string;
+  locationName?: string;
+  phone?: string;
+  registrationDate?: string;
+  accountId?: string;
+  certification?: number;
+  role: RoleType;
+}

+ 10 - 0
simulation-ui/src/types/echarts.ts

@@ -0,0 +1,10 @@
+import { CallbackDataParams } from 'echarts/types/dist/shared';
+
+export interface ToolTipFormatterParams extends CallbackDataParams {
+  axisDim: string;
+  axisIndex: number;
+  axisType: string;
+  axisId: string;
+  axisValue: string;
+  axisValueLabel: string;
+}

+ 37 - 0
simulation-ui/src/types/global.ts

@@ -0,0 +1,37 @@
+export interface AnyObject {
+  [key: string]: unknown;
+}
+
+export interface Options {
+  value: unknown;
+  label: string;
+}
+
+export interface NodeOptions extends Options {
+  children?: NodeOptions[];
+}
+
+export interface GetParams {
+  body: null;
+  type: string;
+  url: string;
+}
+
+export interface PostData {
+  body: string;
+  type: string;
+  url: string;
+}
+
+export interface Pagination {
+  current: number;
+  pageSize: number;
+  total?: number;
+}
+
+export type TimeRanger = [string, string];
+
+export interface GeneralChart {
+  xAxis: string[];
+  data: Array<{ name: string; value: number[] }>;
+}

+ 5 - 0
simulation-ui/src/types/mock.ts

@@ -0,0 +1,5 @@
+export interface MockParams {
+  url: string;
+  type: string;
+  body: string;
+}

+ 17 - 0
simulation-ui/src/utils/auth.ts

@@ -0,0 +1,17 @@
+const isLogin = () => {
+  return !!localStorage.getItem('token');
+};
+
+const getToken = () => {
+  return localStorage.getItem('token');
+};
+
+const setToken = (token: string) => {
+  localStorage.setItem('token', token);
+};
+
+const clearToken = () => {
+  localStorage.removeItem('token');
+};
+
+export { isLogin, getToken, setToken, clearToken };

+ 3 - 0
simulation-ui/src/utils/env.ts

@@ -0,0 +1,3 @@
+const debug = true;
+
+export default debug;

+ 28 - 0
simulation-ui/src/utils/monitor.ts

@@ -0,0 +1,28 @@
+import { App, ComponentPublicInstance } from 'vue';
+import axios from 'axios';
+
+export default function handleError(Vue: App, baseUrl: string) {
+  if (!baseUrl) {
+    return;
+  }
+  Vue.config.errorHandler = (
+    err: unknown,
+    instance: ComponentPublicInstance | null,
+    info: string
+  ) => {
+    // send error info
+    axios.post(`${baseUrl}/report-error`, {
+      err,
+      instance,
+      info,
+      // location: window.location.href,
+      // message: err.message,
+      // stack: err.stack,
+      // browserInfo: getBrowserInfo(),
+      // user info
+      // dom info
+      // url info
+      // ...
+    });
+  };
+}

+ 23 - 0
simulation-ui/src/utils/setup-mock.ts

@@ -0,0 +1,23 @@
+import debug from './env';
+
+export default ({ mock, setup }: { mock?: boolean; setup: () => void }) => {
+  if (mock !== false && debug) setup();
+};
+
+export const successResponseWrap = (data: unknown) => {
+  return {
+    data,
+    status: 'ok',
+    msg: '请求成功',
+    code: 20000,
+  };
+};
+
+export const failResponseWrap = (data: unknown, msg: string, code = 50000) => {
+  return {
+    data,
+    status: 'fail',
+    msg,
+    code,
+  };
+};

+ 9 - 0
simulation-ui/src/views/dashboard/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <router-view />
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({});
+</script>

+ 96 - 0
simulation-ui/src/views/dashboard/monitor/components/chat-item.vue

@@ -0,0 +1,96 @@
+<template>
+  <div :class="['chat-item', itemData.isCollect ? 'chat-item-collected' : '']">
+    <a-space :size="4" direction="vertical" fill>
+      <a-typography-text type="warning">
+        {{ itemData.username }}
+      </a-typography-text>
+      <a-typography-text>{{ itemData.content }}</a-typography-text>
+      <div class="chat-item-footer">
+        <div class="chat-item-time">
+          <a-typography-text type="secondary">
+            {{ itemData.time }}
+          </a-typography-text>
+        </div>
+        <div class="chat-item-actions">
+          <div class="chat-item-actions-item">
+            <icon-command />
+          </div>
+          <div class="chat-item-actions-item chat-item-actions-collect">
+            <icon-star />
+          </div>
+        </div>
+      </div>
+    </a-space>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+import { ChatRecord } from '@/api/message';
+
+export default defineComponent({
+  props: {
+    itemData: {
+      type: Object as PropType<ChatRecord>,
+      default() {
+        return {};
+      },
+    },
+  },
+});
+</script>
+
+<style scoped lang="less">
+.chat-item {
+  padding: 8px;
+  font-size: 12px;
+  line-height: 20px;
+  border-radius: 2px;
+
+  &-footer {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+
+  &-actions {
+    display: flex;
+    opacity: 0;
+
+    &-item {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 20px;
+      height: 20px;
+      margin-right: 4px;
+      color: var(--color-text-3);
+      font-size: 14px;
+      border-radius: 50%;
+      cursor: pointer;
+
+      &:hover {
+        background-color: rgb(var(--gray-3));
+      }
+
+      &:last-child {
+        margin-right: 0;
+      }
+    }
+  }
+
+  &-collected {
+    .chat-item-actions-collect {
+      color: rgb(var(--gold-6));
+    }
+  }
+
+  &:hover {
+    background-color: rgb(var(--gray-2));
+
+    .chat-item-actions {
+      opacity: 1;
+    }
+  }
+}
+</style>

+ 81 - 0
simulation-ui/src/views/dashboard/monitor/components/chat-list.vue

@@ -0,0 +1,81 @@
+<template>
+  <div class="chat-list">
+    <ChatItem v-for="item in renderList" :key="item.id" :item-data="item" />
+    <a-result v-if="!renderList.length" status="404" />
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+import { ChatRecord } from '@/api/message';
+import ChatItem from './chat-item.vue';
+
+export default defineComponent({
+  components: {
+    ChatItem,
+  },
+  props: {
+    renderList: {
+      type: Array as PropType<ChatRecord[]>,
+      default() {
+        return [];
+      },
+    },
+  },
+});
+</script>
+
+<style scoped lang="less">
+.chat-item {
+  padding: 8px;
+  font-size: 12px;
+  line-height: 20px;
+  border-radius: 2px;
+
+  &-footer {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+
+  &-actions {
+    display: flex;
+    opacity: 0;
+
+    &-item {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 20px;
+      height: 20px;
+      margin-right: 4px;
+      color: var(--color-text-3);
+      font-size: 14px;
+      border-radius: 50%;
+      cursor: pointer;
+
+      &:hover {
+        background-color: rgb(var(--gray-3));
+      }
+
+      &:last-child {
+        margin-right: 0;
+      }
+    }
+  }
+
+  &-collected {
+    .message-item-actions-collect {
+      color: rgb(var(--gold-6));
+    }
+  }
+
+  &:hover {
+    background-color: rgb(var(--gray-2));
+
+    .message-item-actions {
+      opacity: 1;
+    }
+  }
+}
+</style>

+ 90 - 0
simulation-ui/src/views/dashboard/monitor/components/chat-panel.vue

@@ -0,0 +1,90 @@
+<template>
+  <a-card
+    class="general-card chat-panel"
+    :title="$t('monitor.title.chatPanel')"
+    :bordered="false"
+    :header-style="{ paddingBottom: '0' }"
+    :body-style="{
+      height: '100%',
+      paddingTop: '16px',
+      display: 'flex',
+      flexFlow: 'column',
+    }"
+  >
+    <a-space :size="8">
+      <a-select style="width: 86px" default-value="all">
+        <a-option value="all">
+          {{ $t('monitor.chat.options.all') }}
+        </a-option>
+      </a-select>
+      <a-input-search
+        :placeholder="$t('monitor.chat.placeholder.searchCategory')"
+      />
+      <a-button type="text">
+        <icon-download />
+      </a-button>
+    </a-space>
+    <div class="chat-panel-content">
+      <a-spin :loading="loading" style="width: 100%">
+        <ChatList :render-list="chatList" />
+      </a-spin>
+    </div>
+    <div class="chat-panel-footer">
+      <a-space :size="8">
+        <a-Input>
+          <template #suffix>
+            <icon-face-smile-fill />
+          </template>
+        </a-Input>
+        <a-button type="primary">{{ $t('monitor.chat.update') }}</a-button>
+      </a-space>
+    </div>
+  </a-card>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue';
+import { queryChatList, ChatRecord } from '@/api/message';
+import useLoading from '@/hooks/loading';
+import ChatList from './chat-list.vue';
+
+export default defineComponent({
+  components: {
+    ChatList,
+  },
+  setup() {
+    const { loading, setLoading } = useLoading(true);
+    const chatList = ref<ChatRecord[]>([]);
+    const fetchData = async () => {
+      try {
+        const { data } = await queryChatList();
+        chatList.value = data;
+      } catch (err) {
+        // you can report use errorHandler or other
+      } finally {
+        setLoading(false);
+      }
+    };
+    fetchData();
+    return {
+      loading,
+      chatList,
+    };
+  },
+});
+</script>
+
+<style scoped lang="less">
+.chat-panel {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  // padding: 20px;
+  background-color: var(--color-bg-2);
+
+  &-content {
+    flex: 1;
+    margin: 20px 0;
+  }
+}
+</style>

+ 141 - 0
simulation-ui/src/views/dashboard/monitor/components/data-statistic-list.vue

@@ -0,0 +1,141 @@
+<template>
+  <div>
+    <a-table
+      :columns="columns"
+      :data="data"
+      row-key="id"
+      :row-selection="{
+        type: 'checkbox',
+        showCheckedAll: true,
+      }"
+      :border="false"
+      :pagination="false"
+    />
+    <a-typography-text type="secondary" class="data-statistic-list-tip">
+      {{ $t('monitor.list.tip.rotations') }} {{ data.length }}
+      {{ $t('monitor.list.tip.rest') }}
+    </a-typography-text>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed, h, compile } from 'vue';
+import { useI18n } from 'vue-i18n';
+import {
+  TableColumn,
+  TableData,
+} from '@arco-design/web-vue/es/table/interface.d';
+
+interface PreviewRecord {
+  cover: string;
+  name: string;
+  duration: string;
+  id: string;
+  status: number;
+}
+export default defineComponent({
+  setup() {
+    const { t } = useI18n();
+    const data: PreviewRecord[] = [
+      {
+        cover:
+          'http://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/c788fc704d32cf3b1136c7d45afc2669.png~tplv-uwbnlip3yd-webp.webp',
+        name: '视频直播',
+        duration: '00:05:19',
+        id: '54e23ade',
+        status: -1,
+      },
+    ];
+    const renderTag = (status: number) => {
+      if (status === -1) {
+        return `<a-tag  color="red" class='data-statistic-list-cover-tag'>
+            ${t('monitor.list.tag.auditFailed')}
+        </a-tag>`;
+      }
+      return '';
+    };
+    // Using the Render function is more flexible than using templates.
+    // But, cannot bind context and local scopes are also lost
+
+    const columns = computed(() => {
+      return [
+        {
+          title: t('monitor.list.title.order'),
+          render({
+            rowIndex,
+          }: {
+            record: TableData;
+            column: TableColumn;
+            rowIndex: number;
+          }) {
+            const tmp = `<span>${rowIndex + 1}</span>`;
+            return h(compile(tmp));
+          },
+        },
+        {
+          title: t('monitor.list.title.cover'),
+          render({
+            record,
+          }: {
+            record: TableData;
+            column: TableColumn;
+            rowIndex: number;
+          }) {
+            const tmp = `<div class='data-statistic-list-cover-wrapper'>
+              <img src=${record.cover} />
+              ${renderTag(record.status)}
+            </div>`;
+            return h(compile(tmp));
+          },
+        },
+        {
+          title: t('monitor.list.title.name'),
+          dataIndex: 'name',
+        },
+        {
+          dataIndex: 'duration',
+          title: t('monitor.list.title.duration'),
+        },
+        {
+          dataIndex: 'id',
+          title: t('monitor.list.title.id'),
+        },
+      ];
+    });
+    return {
+      data,
+      columns,
+    };
+  },
+});
+</script>
+
+<style lang="less">
+// Warning: Here is the global style
+.data-statistic {
+  &-list {
+    &-cover {
+      &-wrapper {
+        position: relative;
+        height: 68px;
+
+        img {
+          height: 100%;
+        }
+      }
+
+      &-tag {
+        position: absolute;
+        top: 6px;
+        left: 6px;
+      }
+    }
+
+    &-tip {
+      display: block;
+      margin-top: 16px;
+      text-align: center;
+    }
+  }
+}
+</style>

+ 63 - 0
simulation-ui/src/views/dashboard/monitor/components/data-statistic.vue

@@ -0,0 +1,63 @@
+<template>
+  <a-card :bordered="false" :body-style="{ padding: '20px' }">
+    <a-tabs default-active-tab="liveMethod">
+      <a-tab-pane
+        key="liveMethod"
+        :title="$t('monitor.tab.title.liveMethod')"
+      />
+      <a-tab-pane
+        key="onlinePopulation"
+        :title="$t('monitor.tab.title.onlinePopulation')"
+      />
+    </a-tabs>
+    <div class="data-statistic-content">
+      <a-radio-group :default-value="3" type="button">
+        <a-radio :value="1">{{ $t('monitor.liveMethod.normal') }}</a-radio>
+        <a-radio :value="2">{{ $t('monitor.liveMethod.flowControl') }}</a-radio>
+        <a-radio :value="3">{{ $t('monitor.liveMethod.video') }}</a-radio>
+        <a-radio :value="4">{{ $t('monitor.liveMethod.web') }}</a-radio>
+      </a-radio-group>
+
+      <div class="data-statistic-list-wrapper">
+        <div class="data-statistic-list-header">
+          <a-button type="text">{{ $t('monitor.editCarousel') }}</a-button>
+          <a-button disabled>{{ $t('monitor.startCarousel') }}</a-button>
+        </div>
+        <div class="data-statistic-list-content">
+          <DataStatisticList />
+        </div>
+      </div>
+    </div>
+  </a-card>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import DataStatisticList from './data-statistic-list.vue';
+
+export default defineComponent({
+  components: {
+    DataStatisticList,
+  },
+});
+</script>
+
+<style scoped lang="less">
+.data-statistic {
+  &-content {
+    padding: 20px 0;
+  }
+
+  &-list {
+    &-header {
+      display: flex;
+      justify-content: space-between;
+      margin-top: 16px;
+    }
+
+    &-content {
+      margin-top: 16px;
+    }
+  }
+}
+</style>

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.