Browse Source

大版本更新

lin-xin 1 year ago
parent
commit
b9a4402431
87 changed files with 6821 additions and 4921 deletions
  1. 23 23
      .gitignore
  2. 13 71
      README.md
  3. 25 1
      components.d.ts
  4. 2 2
      index.html
  5. 0 1298
      package-lock.json
  6. 44 40
      package.json
  7. 46 0
      public/mock/role.json
  8. 40 39
      public/mock/table.json
  9. 23 0
      public/mock/user.json
  10. BIN
      screenshots/wms1.png
  11. BIN
      screenshots/wms3.png
  12. 17 14
      src/App.vue
  13. 22 8
      src/api/index.ts
  14. 0 23
      src/assets/css/color-dark.scss
  15. 95 132
      src/assets/css/main.css
  16. 1 0
      src/assets/img/logo.svg
  17. BIN
      src/assets/img/ucenter-bg.jpg
  18. 39 0
      src/components/countup.vue
  19. 200 154
      src/components/header.vue
  20. 221 0
      src/components/menu.ts
  21. 90 181
      src/components/sidebar.vue
  22. 211 0
      src/components/table-custom.vue
  23. 9 29
      src/components/table-detail.vue
  24. 51 63
      src/components/table-edit.vue
  25. 60 0
      src/components/table-search.vue
  26. 149 0
      src/components/tabs.vue
  27. 0 168
      src/components/tags.vue
  28. 28 28
      src/main.ts
  29. 293 185
      src/router/index.ts
  30. 58 23
      src/store/permiss.ts
  31. 25 15
      src/store/sidebar.ts
  32. 53 53
      src/store/tabs.ts
  33. 58 0
      src/store/theme.ts
  34. 21 0
      src/types/form-option.ts
  35. 9 0
      src/types/menu.ts
  36. 8 0
      src/types/role.ts
  37. 9 0
      src/types/table.ts
  38. 16 0
      src/types/user.ts
  39. 0 0
      src/utils/china.ts
  40. 14 0
      src/utils/index.ts
  41. 31 31
      src/utils/request.ts
  42. 0 54
      src/views/403.vue
  43. 0 54
      src/views/404.vue
  44. 87 0
      src/views/chart/echarts.vue
  45. 345 0
      src/views/chart/options.ts
  46. 129 127
      src/views/chart/schart.vue
  47. 357 301
      src/views/dashboard.vue
  48. 0 14
      src/views/donate.vue
  49. 82 0
      src/views/element/calendar.vue
  50. 66 0
      src/views/element/carousel.vue
  51. 189 0
      src/views/element/form.vue
  52. 340 0
      src/views/element/statistic.vue
  53. 61 0
      src/views/element/steps.vue
  54. 116 0
      src/views/element/tabs.vue
  55. 33 0
      src/views/element/tour.vue
  56. 44 48
      src/views/element/upload.vue
  57. 62 0
      src/views/element/watermark.vue
  58. 0 156
      src/views/form.vue
  59. 56 26
      src/views/home.vue
  60. 67 0
      src/views/pages/403.vue
  61. 67 0
      src/views/pages/404.vue
  62. 0 0
      src/views/pages/editor.vue
  63. 257 212
      src/views/pages/icon.vue
  64. 172 136
      src/views/pages/login.vue
  65. 21 21
      src/views/pages/markdown.vue
  66. 132 0
      src/views/pages/register.vue
  67. 97 0
      src/views/pages/reset-pwd.vue
  68. 205 0
      src/views/pages/theme.vue
  69. 270 0
      src/views/pages/ucenter.vue
  70. 253 0
      src/views/pages/ucenter2.vue
  71. 0 137
      src/views/permission.vue
  72. 144 0
      src/views/system/menu.vue
  73. 76 0
      src/views/system/role-permission.vue
  74. 162 0
      src/views/system/role.vue
  75. 148 0
      src/views/system/user.vue
  76. 0 194
      src/views/table.vue
  77. 169 0
      src/views/table/basetable.vue
  78. 98 98
      src/views/table/export.vue
  79. 109 118
      src/views/table/import.vue
  80. 79 0
      src/views/table/table-editor.vue
  81. 0 116
      src/views/tabs.vue
  82. 0 174
      src/views/user.vue
  83. 10 10
      src/vite-env.d.ts
  84. 22 18
      tsconfig.json
  85. 9 9
      tsconfig.node.json
  86. 31 22
      vite.config.ts
  87. 252 295
      yarn.lock

+ 23 - 23
.gitignore

@@ -1,23 +1,23 @@
-.DS_Store
-node_modules
-/dist
-
-
-# local env files
-.env.local
-.env.*.local
-
-# Log files
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-
-# Editor directories and files
-.idea
-.vscode
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 13 - 71
README.md

@@ -1,19 +1,10 @@
 # vue-manage-system
 
-<a href="https://github.com/vuejs/vue">
-    <img src="https://img.shields.io/badge/vue-3.1.2-brightgreen.svg" alt="vue">
-  </a>
-  <a href="https://github.com/vuejs/pinia">
-    <img src="https://img.shields.io/badge/pinia-2.0.14-brightgreen.svg" alt="pinia">
-  </a>
-  <a href="https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE">
-    <img src="https://img.shields.io/github/license/mashape/apistatus.svg" alt="license">
-  </a>
   <a href="https://github.com/lin-xin/vue-manage-system/releases">
     <img src="https://img.shields.io/github/release/lin-xin/vue-manage-system.svg" alt="GitHub release">
   </a>
-  <a href="https://lin-xin.gitee.io/example/work/#/donate">
-    <img src="https://img.shields.io/badge/%24-donate-ff69b4.svg" alt="donate">
+   <a href="https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE">
+    <img src="https://img.shields.io/github/license/mashape/apistatus.svg" alt="license">
   </a>
 
 基于 Vue3 + pinia + Element Plus 的后台管理系统解决方案。[线上地址](https://lin-xin.gitee.io/example/work/)
@@ -28,7 +19,7 @@
 
 [<img src="https://static.bestqa.net/logo/bestqa_haowen.png" width="220" height="100">](https://www.bestqa.net/home/index.html)
 
-专业问卷服务,一对一客服,按需定制 
+专业问卷服务,一对一客服,按需定制
 
 ## 支持作者
 
@@ -46,21 +37,20 @@
 -   [x] vite 3
 -   [x] pinia
 -   [x] typescript
--   [x] 登录/注
+-   [x] 登录/注
 -   [x] Dashboard
--   [x] 表格
--   [x] Tab 选项卡
--   [x] 表单
+-   [x] 表格/表单
 -   [x] 图表 :bar_chart:
--   [x] 富文本/markdown编辑器
+-   [x] 富文本/markdown 编辑器
 -   [x] 图片拖拽/裁剪上传
 -   [x] 权限管理
 -   [x] 三级菜单
 -   [x] 自定义图标
-
+-   [x] 主题切换
 
 ## 安装步骤
-> 因为使用vite3,node版本需要 14.18+
+
+> 因为使用 vite3,node 版本需要 14.18+
 
 ```
 git clone https://github.com/lin-xin/vue-manage-system.git      // 把模板下载到本地
@@ -74,64 +64,16 @@ npm run dev
 npm run build
 ```
 
-## 组件使用说明与演示
-
-### vue-schart
-
-vue.js 封装 sChart.js 的图表组件。访问地址:[vue-schart](https://github.com/lin-xin/vue-schart#/) 
-
-<p><a href="https://www.npmjs.com/package/vue-schart"><img src="https://img.shields.io/npm/dm/vue-schart.svg" alt="Downloads"></a></p>
-
-```html
-<template>
-    <div>
-        <schart class="wrapper" canvasId="myCanvas" :options="options"></schart>
-    </div>
-</template>
-
-<script setup lang="ts">
-import { ref } from 'vue';
-import Schart from "vue-schart"; // 导入Schart组件
-const options = ref({
-    type: "bar",
-    title: {
-        text: "最近一周各品类销售图",
-    },
-    labels: ["周一", "周二", "周三", "周四", "周五"],
-    datasets: [
-        {
-            label: "家电",
-            data: [234, 278, 270, 190, 230],
-        },
-        {
-            label: "百货",
-            data: [164, 178, 190, 135, 160],
-        },
-        {
-            label: "食品",
-            data: [144, 198, 150, 235, 120],
-        },
-    ],
-})
-</script>
-<style>
-    .wrapper {
-        width: 7rem;
-        height: 5rem;
-    }
-</style>
-```
-
 ## 项目截图
 
-### 登录
-
-![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms3.png)
-
 ### 首页
 
 ![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms1.png)
 
+### 登录
+
+![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms3.png)
+
 ## License
 
 [MIT](https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE)

+ 25 - 1
components.d.ts

@@ -7,17 +7,24 @@ export {}
 
 declare module '@vue/runtime-core' {
   export interface GlobalComponents {
+    Countup: typeof import('./src/components/countup.vue')['default']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElButton: typeof import('element-plus/es')['ElButton']
+    ElCalendar: typeof import('element-plus/es')['ElCalendar']
     ElCard: typeof import('element-plus/es')['ElCard']
+    ElCarousel: typeof import('element-plus/es')['ElCarousel']
+    ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
     ElCascader: typeof import('element-plus/es')['ElCascader']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
+    ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
+    ElCountdown: typeof import('element-plus/es')['ElCountdown']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
     ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
+    ElDivider: typeof import('element-plus/es')['ElDivider']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@@ -26,6 +33,7 @@ declare module '@vue/runtime-core' {
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
@@ -33,9 +41,17 @@ declare module '@vue/runtime-core' {
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElProgress: typeof import('element-plus/es')['ElProgress']
     ElRadio: typeof import('element-plus/es')['ElRadio']
+    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
+    ElRate: typeof import('element-plus/es')['ElRate']
+    ElResult: typeof import('element-plus/es')['ElResult']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElSlider: typeof import('element-plus/es')['ElSlider']
+    ElSpace: typeof import('element-plus/es')['ElSpace']
+    ElStatistic: typeof import('element-plus/es')['ElStatistic']
+    ElStep: typeof import('element-plus/es')['ElStep']
+    ElSteps: typeof import('element-plus/es')['ElSteps']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
@@ -43,15 +59,23 @@ declare module '@vue/runtime-core' {
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
+    ElTimeline: typeof import('element-plus/es')['ElTimeline']
+    ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
     ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
+    ElTour: typeof import('element-plus/es')['ElTour']
+    ElTourStep: typeof import('element-plus/es')['ElTourStep']
+    ElTransfer: typeof import('element-plus/es')['ElTransfer']
     ElUpload: typeof import('element-plus/es')['ElUpload']
+    ElWatermark: typeof import('element-plus/es')['ElWatermark']
     Header: typeof import('./src/components/header.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     Sidebar: typeof import('./src/components/sidebar.vue')['default']
+    TableCustom: typeof import('./src/components/table-custom.vue')['default']
     TableDetail: typeof import('./src/components/table-detail.vue')['default']
     TableEdit: typeof import('./src/components/table-edit.vue')['default']
-    Tags: typeof import('./src/components/tags.vue')['default']
+    TableSearch: typeof import('./src/components/table-search.vue')['default']
+    Tabs: typeof import('./src/components/tabs.vue')['default']
   }
 }

+ 2 - 2
index.html

@@ -6,13 +6,13 @@
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width,initial-scale=1.0">
   <title>vue-manage-system后台管理系统</title>
-  <link rel="stylesheet" href="https://at.alicdn.com/t/font_830376_qzecyukz0s.css">
+  <link rel="stylesheet" href="//at.alicdn.com/t/c/font_830376_92o68tc95je.css">
 </head>
 
 <body>
   <noscript>
     <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
-      Please enable it to continue.</strong>
+        Please enable it to continue.</strong>
   </noscript>
   <div id="app"></div>
   <script type="module" src="/src/main.ts"></script>

+ 0 - 1298
package-lock.json

@@ -1,1298 +0,0 @@
-{
-	"name": "vue-manage-system",
-	"version": "5.3.0",
-	"lockfileVersion": 1,
-	"requires": true,
-	"dependencies": {
-		"@antfu/utils": {
-			"version": "0.5.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@antfu/utils/-/utils-0.5.2.tgz",
-			"integrity": "sha512-CQkeV+oJxUazwjlHD0/3ZD08QWKuGQkhnrKo3e6ly5pd48VUpXbb77q0xMU4+vc2CkJnDS02Eq/M9ugyX20XZA==",
-			"dev": true
-		},
-		"@babel/parser": {
-			"version": "7.18.11",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@babel/parser/-/parser-7.18.11.tgz",
-			"integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ=="
-		},
-		"@babel/runtime": {
-			"version": "7.18.9",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@babel/runtime/-/runtime-7.18.9.tgz",
-			"integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==",
-			"requires": {
-				"regenerator-runtime": "^0.13.4"
-			}
-		},
-		"@babel/runtime-corejs3": {
-			"version": "7.18.9",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@babel/runtime-corejs3/-/runtime-corejs3-7.18.9.tgz",
-			"integrity": "sha512-qZEWeccZCrHA2Au4/X05QW5CMdm4VjUDCrGq5gf1ZDcM4hRqreKrtwAn7yci9zfgAS9apvnsFXiGBHBAxZdK9A==",
-			"requires": {
-				"core-js-pure": "^3.20.2",
-				"regenerator-runtime": "^0.13.4"
-			}
-		},
-		"@ctrl/tinycolor": {
-			"version": "3.4.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz",
-			"integrity": "sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw=="
-		},
-		"@element-plus/icons-vue": {
-			"version": "2.0.9",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@element-plus/icons-vue/-/icons-vue-2.0.9.tgz",
-			"integrity": "sha512-okdrwiVeKBmW41Hkl0eMrXDjzJwhQMuKiBOu17rOszqM+LS/yBYpNQNV5Jvoh06Wc+89fMmb/uhzf8NZuDuUaQ=="
-		},
-		"@esbuild/linux-loong64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz",
-			"integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==",
-			"dev": true,
-			"optional": true
-		},
-		"@floating-ui/core": {
-			"version": "0.7.3",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@floating-ui/core/-/core-0.7.3.tgz",
-			"integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg=="
-		},
-		"@floating-ui/dom": {
-			"version": "0.5.4",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@floating-ui/dom/-/dom-0.5.4.tgz",
-			"integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==",
-			"requires": {
-				"@floating-ui/core": "^0.7.3"
-			}
-		},
-		"@nodelib/fs.scandir": {
-			"version": "2.1.5",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-			"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
-			"dev": true,
-			"requires": {
-				"@nodelib/fs.stat": "2.0.5",
-				"run-parallel": "^1.1.9"
-			}
-		},
-		"@nodelib/fs.stat": {
-			"version": "2.0.5",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
-			"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
-			"dev": true
-		},
-		"@nodelib/fs.walk": {
-			"version": "1.2.8",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
-			"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-			"dev": true,
-			"requires": {
-				"@nodelib/fs.scandir": "2.1.5",
-				"fastq": "^1.6.0"
-			}
-		},
-		"@rollup/pluginutils": {
-			"version": "4.2.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
-			"integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==",
-			"dev": true,
-			"requires": {
-				"estree-walker": "^2.0.1",
-				"picomatch": "^2.2.2"
-			}
-		},
-		"@types/lodash": {
-			"version": "4.14.184",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@types/lodash/-/lodash-4.14.184.tgz",
-			"integrity": "sha512-RoZphVtHbxPZizt4IcILciSWiC6dcn+eZ8oX9IWEYfDMcocdd42f7NPI6fQj+6zI8y4E0L7gu2pcZKLGTRaV9Q=="
-		},
-		"@types/lodash-es": {
-			"version": "4.17.6",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@types/lodash-es/-/lodash-es-4.17.6.tgz",
-			"integrity": "sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==",
-			"requires": {
-				"@types/lodash": "*"
-			}
-		},
-		"@types/marked": {
-			"version": "4.0.5",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@types/marked/-/marked-4.0.5.tgz",
-			"integrity": "sha512-jMN2moJ+lSf1VZXQo3VXeMCjoXuciVONig8+U0YNBop5aBvQw4qkolx1Nzn1i0T8L2l9IZ3jju6bS1pPwlaY1w=="
-		},
-		"@types/web-bluetooth": {
-			"version": "0.0.15",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz",
-			"integrity": "sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA=="
-		},
-		"@vitejs/plugin-vue": {
-			"version": "3.0.3",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vitejs/plugin-vue/-/plugin-vue-3.0.3.tgz",
-			"integrity": "sha512-U4zNBlz9mg+TA+i+5QPc3N5lQvdUXENZLO2h0Wdzp56gI1MWhqJOv+6R+d4kOzoaSSq6TnGPBdZAXKOe4lXy6g==",
-			"dev": true
-		},
-		"@volar/code-gen": {
-			"version": "0.38.9",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@volar/code-gen/-/code-gen-0.38.9.tgz",
-			"integrity": "sha512-n6LClucfA+37rQeskvh9vDoZV1VvCVNy++MAPKj2dT4FT+Fbmty/SDQqnsEBtdEe6E3OQctFvA/IcKsx3Mns0A==",
-			"dev": true,
-			"requires": {
-				"@volar/source-map": "0.38.9"
-			}
-		},
-		"@volar/source-map": {
-			"version": "0.38.9",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@volar/source-map/-/source-map-0.38.9.tgz",
-			"integrity": "sha512-ba0UFoHDYry+vwKdgkWJ6xlQT+8TFtZg1zj9tSjj4PykW1JZDuM0xplMotLun4h3YOoYfY9K1huY5gvxmrNLIw==",
-			"dev": true
-		},
-		"@volar/vue-code-gen": {
-			"version": "0.38.9",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@volar/vue-code-gen/-/vue-code-gen-0.38.9.tgz",
-			"integrity": "sha512-tzj7AoarFBKl7e41MR006ncrEmNPHALuk8aG4WdDIaG387X5//5KhWC5Ff3ZfB2InGSeNT+CVUd74M0gS20rjA==",
-			"dev": true,
-			"requires": {
-				"@volar/code-gen": "0.38.9",
-				"@volar/source-map": "0.38.9",
-				"@vue/compiler-core": "^3.2.37",
-				"@vue/compiler-dom": "^3.2.37",
-				"@vue/shared": "^3.2.37"
-			}
-		},
-		"@volar/vue-typescript": {
-			"version": "0.38.9",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@volar/vue-typescript/-/vue-typescript-0.38.9.tgz",
-			"integrity": "sha512-iJMQGU91ADi98u8V1vXd2UBmELDAaeSP0ZJaFjwosClQdKlJQYc6MlxxKfXBZisHqfbhdtrGRyaryulnYtliZw==",
-			"dev": true,
-			"requires": {
-				"@volar/code-gen": "0.38.9",
-				"@volar/source-map": "0.38.9",
-				"@volar/vue-code-gen": "0.38.9",
-				"@vue/compiler-sfc": "^3.2.37",
-				"@vue/reactivity": "^3.2.37"
-			}
-		},
-		"@vue/compiler-core": {
-			"version": "3.2.37",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vue/compiler-core/-/compiler-core-3.2.37.tgz",
-			"integrity": "sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==",
-			"requires": {
-				"@babel/parser": "^7.16.4",
-				"@vue/shared": "3.2.37",
-				"estree-walker": "^2.0.2",
-				"source-map": "^0.6.1"
-			}
-		},
-		"@vue/compiler-dom": {
-			"version": "3.2.37",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz",
-			"integrity": "sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==",
-			"requires": {
-				"@vue/compiler-core": "3.2.37",
-				"@vue/shared": "3.2.37"
-			}
-		},
-		"@vue/compiler-sfc": {
-			"version": "3.2.37",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz",
-			"integrity": "sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==",
-			"requires": {
-				"@babel/parser": "^7.16.4",
-				"@vue/compiler-core": "3.2.37",
-				"@vue/compiler-dom": "3.2.37",
-				"@vue/compiler-ssr": "3.2.37",
-				"@vue/reactivity-transform": "3.2.37",
-				"@vue/shared": "3.2.37",
-				"estree-walker": "^2.0.2",
-				"magic-string": "^0.25.7",
-				"postcss": "^8.1.10",
-				"source-map": "^0.6.1"
-			},
-			"dependencies": {
-				"magic-string": {
-					"version": "0.25.9",
-					"resolved": "https://repo.huaweicloud.com/repository/npm/magic-string/-/magic-string-0.25.9.tgz",
-					"integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
-					"requires": {
-						"sourcemap-codec": "^1.4.8"
-					}
-				}
-			}
-		},
-		"@vue/compiler-ssr": {
-			"version": "3.2.37",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz",
-			"integrity": "sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==",
-			"requires": {
-				"@vue/compiler-dom": "3.2.37",
-				"@vue/shared": "3.2.37"
-			}
-		},
-		"@vue/devtools-api": {
-			"version": "6.2.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vue/devtools-api/-/devtools-api-6.2.1.tgz",
-			"integrity": "sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ=="
-		},
-		"@vue/reactivity": {
-			"version": "3.2.37",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vue/reactivity/-/reactivity-3.2.37.tgz",
-			"integrity": "sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==",
-			"requires": {
-				"@vue/shared": "3.2.37"
-			}
-		},
-		"@vue/reactivity-transform": {
-			"version": "3.2.37",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz",
-			"integrity": "sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==",
-			"requires": {
-				"@babel/parser": "^7.16.4",
-				"@vue/compiler-core": "3.2.37",
-				"@vue/shared": "3.2.37",
-				"estree-walker": "^2.0.2",
-				"magic-string": "^0.25.7"
-			},
-			"dependencies": {
-				"magic-string": {
-					"version": "0.25.9",
-					"resolved": "https://repo.huaweicloud.com/repository/npm/magic-string/-/magic-string-0.25.9.tgz",
-					"integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
-					"requires": {
-						"sourcemap-codec": "^1.4.8"
-					}
-				}
-			}
-		},
-		"@vue/runtime-core": {
-			"version": "3.2.37",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vue/runtime-core/-/runtime-core-3.2.37.tgz",
-			"integrity": "sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==",
-			"requires": {
-				"@vue/reactivity": "3.2.37",
-				"@vue/shared": "3.2.37"
-			}
-		},
-		"@vue/runtime-dom": {
-			"version": "3.2.37",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz",
-			"integrity": "sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==",
-			"requires": {
-				"@vue/runtime-core": "3.2.37",
-				"@vue/shared": "3.2.37",
-				"csstype": "^2.6.8"
-			}
-		},
-		"@vue/server-renderer": {
-			"version": "3.2.37",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vue/server-renderer/-/server-renderer-3.2.37.tgz",
-			"integrity": "sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==",
-			"requires": {
-				"@vue/compiler-ssr": "3.2.37",
-				"@vue/shared": "3.2.37"
-			}
-		},
-		"@vue/shared": {
-			"version": "3.2.37",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vue/shared/-/shared-3.2.37.tgz",
-			"integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw=="
-		},
-		"@vueuse/core": {
-			"version": "9.1.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/core/-/core-9.1.0.tgz",
-			"integrity": "sha512-BIroqvXEqt826aE9r3K5cox1zobuPuAzdYJ36kouC2TVhlXvFKIILgFVWrpp9HZPwB3aLzasmG3K87q7TSyXZg==",
-			"requires": {
-				"@types/web-bluetooth": "^0.0.15",
-				"@vueuse/metadata": "9.1.0",
-				"@vueuse/shared": "9.1.0",
-				"vue-demi": "*"
-			}
-		},
-		"@vueuse/metadata": {
-			"version": "9.1.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/metadata/-/metadata-9.1.0.tgz",
-			"integrity": "sha512-8OEhlog1iaAGTD3LICZ8oBGQdYeMwByvXetOtAOZCJOzyCRSwqwdggTsmVZZ1rkgYIEqgUBk942AsAPwM21s6A=="
-		},
-		"@vueuse/shared": {
-			"version": "9.1.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/@vueuse/shared/-/shared-9.1.0.tgz",
-			"integrity": "sha512-pB/3njQu4tfJJ78ajELNda0yMG6lKfpToQW7Soe09CprF1k3QuyoNi1tBNvo75wBDJWD+LOnr+c4B5HZ39jY/Q==",
-			"requires": {
-				"vue-demi": "*"
-			}
-		},
-		"acorn": {
-			"version": "8.8.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/acorn/-/acorn-8.8.0.tgz",
-			"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
-			"dev": true
-		},
-		"adler-32": {
-			"version": "1.3.1",
-			"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
-			"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="
-		},
-		"anymatch": {
-			"version": "3.1.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/anymatch/-/anymatch-3.1.2.tgz",
-			"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
-			"dev": true,
-			"requires": {
-				"normalize-path": "^3.0.0",
-				"picomatch": "^2.0.4"
-			}
-		},
-		"async-validator": {
-			"version": "4.2.5",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/async-validator/-/async-validator-4.2.5.tgz",
-			"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="
-		},
-		"asynckit": {
-			"version": "0.4.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/asynckit/-/asynckit-0.4.0.tgz",
-			"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
-		},
-		"axios": {
-			"version": "0.27.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/axios/-/axios-0.27.2.tgz",
-			"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
-			"requires": {
-				"follow-redirects": "^1.14.9",
-				"form-data": "^4.0.0"
-			}
-		},
-		"balanced-match": {
-			"version": "1.0.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/balanced-match/-/balanced-match-1.0.2.tgz",
-			"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-			"dev": true
-		},
-		"binary-extensions": {
-			"version": "2.2.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/binary-extensions/-/binary-extensions-2.2.0.tgz",
-			"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
-			"dev": true
-		},
-		"brace-expansion": {
-			"version": "2.0.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/brace-expansion/-/brace-expansion-2.0.1.tgz",
-			"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-			"dev": true,
-			"requires": {
-				"balanced-match": "^1.0.0"
-			}
-		},
-		"braces": {
-			"version": "3.0.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/braces/-/braces-3.0.2.tgz",
-			"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-			"dev": true,
-			"requires": {
-				"fill-range": "^7.0.1"
-			}
-		},
-		"cfb": {
-			"version": "1.2.2",
-			"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
-			"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
-			"requires": {
-				"adler-32": "~1.3.0",
-				"crc-32": "~1.2.0"
-			}
-		},
-		"chokidar": {
-			"version": "3.5.3",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/chokidar/-/chokidar-3.5.3.tgz",
-			"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
-			"dev": true,
-			"requires": {
-				"anymatch": "~3.1.2",
-				"braces": "~3.0.2",
-				"fsevents": "~2.3.2",
-				"glob-parent": "~5.1.2",
-				"is-binary-path": "~2.1.0",
-				"is-glob": "~4.0.1",
-				"normalize-path": "~3.0.0",
-				"readdirp": "~3.6.0"
-			}
-		},
-		"codepage": {
-			"version": "1.15.0",
-			"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
-			"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="
-		},
-		"combined-stream": {
-			"version": "1.0.8",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/combined-stream/-/combined-stream-1.0.8.tgz",
-			"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-			"requires": {
-				"delayed-stream": "~1.0.0"
-			}
-		},
-		"core-js-pure": {
-			"version": "3.24.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/core-js-pure/-/core-js-pure-3.24.1.tgz",
-			"integrity": "sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg=="
-		},
-		"crc-32": {
-			"version": "1.2.2",
-			"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
-			"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="
-		},
-		"cropperjs": {
-			"version": "1.5.12",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/cropperjs/-/cropperjs-1.5.12.tgz",
-			"integrity": "sha512-re7UdjE5UnwdrovyhNzZ6gathI4Rs3KGCBSc8HCIjUo5hO42CtzyblmWLj6QWVw7huHyDMfpKxhiO2II77nhDw=="
-		},
-		"csstype": {
-			"version": "2.6.20",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/csstype/-/csstype-2.6.20.tgz",
-			"integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
-		},
-		"dayjs": {
-			"version": "1.11.5",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/dayjs/-/dayjs-1.11.5.tgz",
-			"integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA=="
-		},
-		"debug": {
-			"version": "4.3.4",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/debug/-/debug-4.3.4.tgz",
-			"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
-			"dev": true,
-			"requires": {
-				"ms": "2.1.2"
-			}
-		},
-		"delayed-stream": {
-			"version": "1.0.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/delayed-stream/-/delayed-stream-1.0.0.tgz",
-			"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
-		},
-		"element-plus": {
-			"version": "2.2.14",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/element-plus/-/element-plus-2.2.14.tgz",
-			"integrity": "sha512-V5Pis0OHhePg1RgVogZrcefaVl8vjVn4Pn9Qsh/t2CbFgjg9kKOYFqf/tuP3ObSXGm3X89hpe0W+nLVAsaFnpw==",
-			"requires": {
-				"@ctrl/tinycolor": "^3.4.1",
-				"@element-plus/icons-vue": "^2.0.6",
-				"@floating-ui/dom": "^0.5.4",
-				"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
-				"@types/lodash": "^4.14.182",
-				"@types/lodash-es": "^4.17.6",
-				"@vueuse/core": "^9.1.0",
-				"async-validator": "^4.2.5",
-				"dayjs": "^1.11.3",
-				"escape-html": "^1.0.3",
-				"lodash": "^4.17.21",
-				"lodash-es": "^4.17.21",
-				"lodash-unified": "^1.0.2",
-				"memoize-one": "^6.0.0",
-				"normalize-wheel-es": "^1.2.0"
-			},
-			"dependencies": {
-				"@popperjs/core": {
-					"version": "npm:@sxzz/popperjs-es@2.11.7",
-					"resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
-					"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ=="
-				}
-			}
-		},
-		"esbuild": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild/-/esbuild-0.14.54.tgz",
-			"integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
-			"dev": true,
-			"requires": {
-				"@esbuild/linux-loong64": "0.14.54",
-				"esbuild-android-64": "0.14.54",
-				"esbuild-android-arm64": "0.14.54",
-				"esbuild-darwin-64": "0.14.54",
-				"esbuild-darwin-arm64": "0.14.54",
-				"esbuild-freebsd-64": "0.14.54",
-				"esbuild-freebsd-arm64": "0.14.54",
-				"esbuild-linux-32": "0.14.54",
-				"esbuild-linux-64": "0.14.54",
-				"esbuild-linux-arm": "0.14.54",
-				"esbuild-linux-arm64": "0.14.54",
-				"esbuild-linux-mips64le": "0.14.54",
-				"esbuild-linux-ppc64le": "0.14.54",
-				"esbuild-linux-riscv64": "0.14.54",
-				"esbuild-linux-s390x": "0.14.54",
-				"esbuild-netbsd-64": "0.14.54",
-				"esbuild-openbsd-64": "0.14.54",
-				"esbuild-sunos-64": "0.14.54",
-				"esbuild-windows-32": "0.14.54",
-				"esbuild-windows-64": "0.14.54",
-				"esbuild-windows-arm64": "0.14.54"
-			}
-		},
-		"esbuild-android-64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz",
-			"integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-android-arm64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz",
-			"integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-darwin-64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz",
-			"integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-darwin-arm64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz",
-			"integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-freebsd-64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz",
-			"integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-freebsd-arm64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz",
-			"integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-linux-32": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz",
-			"integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-linux-64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
-			"integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-linux-arm": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz",
-			"integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-linux-arm64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz",
-			"integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-linux-mips64le": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz",
-			"integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-linux-ppc64le": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz",
-			"integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-linux-riscv64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz",
-			"integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-linux-s390x": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz",
-			"integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-netbsd-64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz",
-			"integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-openbsd-64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz",
-			"integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-sunos-64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz",
-			"integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-windows-32": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz",
-			"integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-windows-64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz",
-			"integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==",
-			"dev": true,
-			"optional": true
-		},
-		"esbuild-windows-arm64": {
-			"version": "0.14.54",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz",
-			"integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==",
-			"dev": true,
-			"optional": true
-		},
-		"escape-html": {
-			"version": "1.0.3",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/escape-html/-/escape-html-1.0.3.tgz",
-			"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
-		},
-		"escape-string-regexp": {
-			"version": "5.0.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
-			"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
-			"dev": true
-		},
-		"estree-walker": {
-			"version": "2.0.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/estree-walker/-/estree-walker-2.0.2.tgz",
-			"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
-		},
-		"fast-glob": {
-			"version": "3.2.11",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/fast-glob/-/fast-glob-3.2.11.tgz",
-			"integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
-			"dev": true,
-			"requires": {
-				"@nodelib/fs.stat": "^2.0.2",
-				"@nodelib/fs.walk": "^1.2.3",
-				"glob-parent": "^5.1.2",
-				"merge2": "^1.3.0",
-				"micromatch": "^4.0.4"
-			}
-		},
-		"fastq": {
-			"version": "1.13.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/fastq/-/fastq-1.13.0.tgz",
-			"integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
-			"dev": true,
-			"requires": {
-				"reusify": "^1.0.4"
-			}
-		},
-		"fill-range": {
-			"version": "7.0.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/fill-range/-/fill-range-7.0.1.tgz",
-			"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-			"dev": true,
-			"requires": {
-				"to-regex-range": "^5.0.1"
-			}
-		},
-		"follow-redirects": {
-			"version": "1.15.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/follow-redirects/-/follow-redirects-1.15.1.tgz",
-			"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
-		},
-		"form-data": {
-			"version": "4.0.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/form-data/-/form-data-4.0.0.tgz",
-			"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
-			"requires": {
-				"asynckit": "^0.4.0",
-				"combined-stream": "^1.0.8",
-				"mime-types": "^2.1.12"
-			}
-		},
-		"frac": {
-			"version": "1.1.2",
-			"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
-			"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="
-		},
-		"fsevents": {
-			"version": "2.3.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/fsevents/-/fsevents-2.3.2.tgz",
-			"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-			"dev": true,
-			"optional": true
-		},
-		"function-bind": {
-			"version": "1.1.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/function-bind/-/function-bind-1.1.1.tgz",
-			"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-			"dev": true
-		},
-		"glob-parent": {
-			"version": "5.1.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/glob-parent/-/glob-parent-5.1.2.tgz",
-			"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-			"dev": true,
-			"requires": {
-				"is-glob": "^4.0.1"
-			}
-		},
-		"has": {
-			"version": "1.0.3",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/has/-/has-1.0.3.tgz",
-			"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-			"dev": true,
-			"requires": {
-				"function-bind": "^1.1.1"
-			}
-		},
-		"is-binary-path": {
-			"version": "2.1.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/is-binary-path/-/is-binary-path-2.1.0.tgz",
-			"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-			"dev": true,
-			"requires": {
-				"binary-extensions": "^2.0.0"
-			}
-		},
-		"is-core-module": {
-			"version": "2.10.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/is-core-module/-/is-core-module-2.10.0.tgz",
-			"integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==",
-			"dev": true,
-			"requires": {
-				"has": "^1.0.3"
-			}
-		},
-		"is-extglob": {
-			"version": "2.1.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/is-extglob/-/is-extglob-2.1.1.tgz",
-			"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-			"dev": true
-		},
-		"is-glob": {
-			"version": "4.0.3",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/is-glob/-/is-glob-4.0.3.tgz",
-			"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-			"dev": true,
-			"requires": {
-				"is-extglob": "^2.1.1"
-			}
-		},
-		"is-number": {
-			"version": "7.0.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/is-number/-/is-number-7.0.0.tgz",
-			"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-			"dev": true
-		},
-		"jsonc-parser": {
-			"version": "3.1.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/jsonc-parser/-/jsonc-parser-3.1.0.tgz",
-			"integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==",
-			"dev": true
-		},
-		"local-pkg": {
-			"version": "0.4.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/local-pkg/-/local-pkg-0.4.2.tgz",
-			"integrity": "sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==",
-			"dev": true
-		},
-		"lodash": {
-			"version": "4.17.21",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/lodash/-/lodash-4.17.21.tgz",
-			"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
-		},
-		"lodash-es": {
-			"version": "4.17.21",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/lodash-es/-/lodash-es-4.17.21.tgz",
-			"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
-		},
-		"lodash-unified": {
-			"version": "1.0.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/lodash-unified/-/lodash-unified-1.0.2.tgz",
-			"integrity": "sha512-OGbEy+1P+UT26CYi4opY4gebD8cWRDxAT6MAObIVQMiqYdxZr1g3QHWCToVsm31x2NkLS4K3+MC2qInaRMa39g=="
-		},
-		"magic-string": {
-			"version": "0.26.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/magic-string/-/magic-string-0.26.2.tgz",
-			"integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==",
-			"dev": true,
-			"requires": {
-				"sourcemap-codec": "^1.4.8"
-			}
-		},
-		"md-editor-v3": {
-			"version": "2.2.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/md-editor-v3/-/md-editor-v3-2.2.1.tgz",
-			"integrity": "sha512-nOd8mlEhvC99l9Y8pDMwEi6EdCAeBp88Ffl24We+2uz3/Iympctm92L1qyNicONJRhtZJacQ4oWTQGMYKGAxVg==",
-			"requires": {
-				"@types/marked": "^4.0.3"
-			}
-		},
-		"memoize-one": {
-			"version": "6.0.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/memoize-one/-/memoize-one-6.0.0.tgz",
-			"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
-		},
-		"merge2": {
-			"version": "1.4.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/merge2/-/merge2-1.4.1.tgz",
-			"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
-			"dev": true
-		},
-		"micromatch": {
-			"version": "4.0.5",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/micromatch/-/micromatch-4.0.5.tgz",
-			"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
-			"dev": true,
-			"requires": {
-				"braces": "^3.0.2",
-				"picomatch": "^2.3.1"
-			}
-		},
-		"mime-db": {
-			"version": "1.52.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/mime-db/-/mime-db-1.52.0.tgz",
-			"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
-		},
-		"mime-types": {
-			"version": "2.1.35",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/mime-types/-/mime-types-2.1.35.tgz",
-			"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-			"requires": {
-				"mime-db": "1.52.0"
-			}
-		},
-		"minimatch": {
-			"version": "5.1.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/minimatch/-/minimatch-5.1.0.tgz",
-			"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
-			"dev": true,
-			"requires": {
-				"brace-expansion": "^2.0.1"
-			}
-		},
-		"mlly": {
-			"version": "0.5.13",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/mlly/-/mlly-0.5.13.tgz",
-			"integrity": "sha512-0SK2fqoan+PMjADs4I2egAtrtNtpjqRez6PDTCeAdGjUQNJCvO5o9v2NEq52WA1jFmMU97qBr/JgdvCquehDbA==",
-			"dev": true,
-			"requires": {
-				"acorn": "^8.8.0",
-				"pathe": "^0.3.4",
-				"pkg-types": "^0.3.3",
-				"ufo": "^0.8.5"
-			}
-		},
-		"ms": {
-			"version": "2.1.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/ms/-/ms-2.1.2.tgz",
-			"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-			"dev": true
-		},
-		"nanoid": {
-			"version": "3.3.4",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.4.tgz",
-			"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
-		},
-		"normalize-path": {
-			"version": "3.0.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/normalize-path/-/normalize-path-3.0.0.tgz",
-			"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-			"dev": true
-		},
-		"normalize-wheel-es": {
-			"version": "1.2.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
-			"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw=="
-		},
-		"path-parse": {
-			"version": "1.0.7",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/path-parse/-/path-parse-1.0.7.tgz",
-			"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
-			"dev": true
-		},
-		"pathe": {
-			"version": "0.3.5",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/pathe/-/pathe-0.3.5.tgz",
-			"integrity": "sha512-grU/QeYP0ChuE5kjU2/k8VtAeODzbernHlue0gTa27+ayGIu3wqYBIPGfP9r5xSqgCgDd4nWrjKXEfxMillByg==",
-			"dev": true
-		},
-		"picocolors": {
-			"version": "1.0.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/picocolors/-/picocolors-1.0.0.tgz",
-			"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
-		},
-		"picomatch": {
-			"version": "2.3.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/picomatch/-/picomatch-2.3.1.tgz",
-			"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-			"dev": true
-		},
-		"pinia": {
-			"version": "2.0.20",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/pinia/-/pinia-2.0.20.tgz",
-			"integrity": "sha512-fdHHumXW/0U5HhxmY1emo3I4z85p8NJPdbtFQSlmJXFe3ktuF0pYNVgVtk2q+j2zCtTufY763xzaEMx0t6T59g==",
-			"requires": {
-				"@vue/devtools-api": "^6.2.1",
-				"vue-demi": "*"
-			}
-		},
-		"pkg-types": {
-			"version": "0.3.4",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/pkg-types/-/pkg-types-0.3.4.tgz",
-			"integrity": "sha512-s214f/xkRpwlwVBToWq9Mu0XlU3HhZMYCnr2var8+jjbavBHh/VCh4pBLsJW29rJ//B1jb4HlpMIaNIMH+W2/w==",
-			"dev": true,
-			"requires": {
-				"jsonc-parser": "^3.1.0",
-				"mlly": "^0.5.13",
-				"pathe": "^0.3.5"
-			}
-		},
-		"postcss": {
-			"version": "8.4.16",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/postcss/-/postcss-8.4.16.tgz",
-			"integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
-			"requires": {
-				"nanoid": "^3.3.4",
-				"picocolors": "^1.0.0",
-				"source-map-js": "^1.0.2"
-			}
-		},
-		"queue-microtask": {
-			"version": "1.2.3",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/queue-microtask/-/queue-microtask-1.2.3.tgz",
-			"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-			"dev": true
-		},
-		"readdirp": {
-			"version": "3.6.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/readdirp/-/readdirp-3.6.0.tgz",
-			"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
-			"dev": true,
-			"requires": {
-				"picomatch": "^2.2.1"
-			}
-		},
-		"regenerator-runtime": {
-			"version": "0.13.9",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
-			"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
-		},
-		"resolve": {
-			"version": "1.22.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/resolve/-/resolve-1.22.1.tgz",
-			"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
-			"dev": true,
-			"requires": {
-				"is-core-module": "^2.9.0",
-				"path-parse": "^1.0.7",
-				"supports-preserve-symlinks-flag": "^1.0.0"
-			}
-		},
-		"reusify": {
-			"version": "1.0.4",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/reusify/-/reusify-1.0.4.tgz",
-			"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
-			"dev": true
-		},
-		"rollup": {
-			"version": "2.77.3",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/rollup/-/rollup-2.77.3.tgz",
-			"integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==",
-			"dev": true,
-			"requires": {
-				"fsevents": "~2.3.2"
-			}
-		},
-		"run-parallel": {
-			"version": "1.2.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/run-parallel/-/run-parallel-1.2.0.tgz",
-			"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-			"dev": true,
-			"requires": {
-				"queue-microtask": "^1.2.2"
-			}
-		},
-		"schart.js": {
-			"version": "3.0.4",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/schart.js/-/schart.js-3.0.4.tgz",
-			"integrity": "sha512-uylb2u9rrHX1jyAuSAJUQON8XTfyDKI9kWj1J3fUlCQCkLVZ4HG4+IiV8qm//Z71dqvLI78QZ/fCBw0reB22Zw=="
-		},
-		"scule": {
-			"version": "0.3.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/scule/-/scule-0.3.2.tgz",
-			"integrity": "sha512-zIvPdjOH8fv8CgrPT5eqtxHQXmPNnV/vHJYffZhE43KZkvULvpCTvOt1HPlFaCZx287INL9qaqrZg34e8NgI4g==",
-			"dev": true
-		},
-		"source-map": {
-			"version": "0.6.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/source-map/-/source-map-0.6.1.tgz",
-			"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
-		},
-		"source-map-js": {
-			"version": "1.0.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/source-map-js/-/source-map-js-1.0.2.tgz",
-			"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
-		},
-		"sourcemap-codec": {
-			"version": "1.4.8",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
-			"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
-		},
-		"ssf": {
-			"version": "0.11.2",
-			"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
-			"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
-			"requires": {
-				"frac": "~1.1.2"
-			}
-		},
-		"strip-literal": {
-			"version": "0.4.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/strip-literal/-/strip-literal-0.4.0.tgz",
-			"integrity": "sha512-ql/sBDoJOybTKSIOWrrh8kgUEMjXMwRAkZTD0EwiwxQH/6tTPkZvMIEjp0CRlpi6V5FMiJyvxeRkEi1KrGISoA==",
-			"dev": true,
-			"requires": {
-				"acorn": "^8.7.1"
-			}
-		},
-		"supports-preserve-symlinks-flag": {
-			"version": "1.0.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
-			"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
-			"dev": true
-		},
-		"to-regex-range": {
-			"version": "5.0.1",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/to-regex-range/-/to-regex-range-5.0.1.tgz",
-			"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-			"dev": true,
-			"requires": {
-				"is-number": "^7.0.0"
-			}
-		},
-		"tslib": {
-			"version": "2.4.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/tslib/-/tslib-2.4.0.tgz",
-			"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
-		},
-		"typescript": {
-			"version": "4.7.4",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/typescript/-/typescript-4.7.4.tgz",
-			"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
-			"dev": true
-		},
-		"ufo": {
-			"version": "0.8.5",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/ufo/-/ufo-0.8.5.tgz",
-			"integrity": "sha512-e4+UtA5IRO+ha6hYklwj6r7BjiGMxS0O+UaSg9HbaTefg4kMkzj4tXzEBajRR+wkxf+golgAWKzLbytCUDMJAA==",
-			"dev": true
-		},
-		"unimport": {
-			"version": "0.6.7",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/unimport/-/unimport-0.6.7.tgz",
-			"integrity": "sha512-EMoVqDjswHkU+nD098QYHXH7Mkw7KwGDQAyeRF2lgairJnuO+wpkhIcmCqrD1OPJmsjkTbJ2tW6Ap8St0PuWZA==",
-			"dev": true,
-			"requires": {
-				"@rollup/pluginutils": "^4.2.1",
-				"escape-string-regexp": "^5.0.0",
-				"fast-glob": "^3.2.11",
-				"local-pkg": "^0.4.2",
-				"magic-string": "^0.26.2",
-				"mlly": "^0.5.7",
-				"pathe": "^0.3.3",
-				"scule": "^0.3.2",
-				"strip-literal": "^0.4.0",
-				"unplugin": "^0.9.0"
-			}
-		},
-		"unplugin": {
-			"version": "0.9.3",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/unplugin/-/unplugin-0.9.3.tgz",
-			"integrity": "sha512-GWXxizZG+tobNs8fuGTCeilerkkfZTZax2iivuE4pxLaF9wTnPJHOq8tbLKDb5ohVb+2BXNjrU9xx59yWTUnuw==",
-			"dev": true,
-			"requires": {
-				"acorn": "^8.8.0",
-				"chokidar": "^3.5.3",
-				"webpack-sources": "^3.2.3",
-				"webpack-virtual-modules": "^0.4.4"
-			}
-		},
-		"unplugin-auto-import": {
-			"version": "0.11.2",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/unplugin-auto-import/-/unplugin-auto-import-0.11.2.tgz",
-			"integrity": "sha512-1+VwBfn9dtiYv9SQLKP1AvZolUbK9xTVeAT+iOcEk4EHSFUlmIqBVLEKI76cifSQTLOJ3rZyPrEgptf3SZNLlQ==",
-			"dev": true,
-			"requires": {
-				"@antfu/utils": "^0.5.2",
-				"@rollup/pluginutils": "^4.2.1",
-				"local-pkg": "^0.4.2",
-				"magic-string": "^0.26.2",
-				"unimport": "^0.6.7",
-				"unplugin": "^0.9.3"
-			}
-		},
-		"unplugin-vue-components": {
-			"version": "0.22.4",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/unplugin-vue-components/-/unplugin-vue-components-0.22.4.tgz",
-			"integrity": "sha512-2rRZcM9OnJGXnYxQNfaceEYuPeVACcWySIjy8WBwIiN3onr980TmA3XE5pRJFt8zoQrUA+c46oyIq96noLqrEQ==",
-			"dev": true,
-			"requires": {
-				"@antfu/utils": "^0.5.2",
-				"@rollup/pluginutils": "^4.2.1",
-				"chokidar": "^3.5.3",
-				"debug": "^4.3.4",
-				"fast-glob": "^3.2.11",
-				"local-pkg": "^0.4.2",
-				"magic-string": "^0.26.2",
-				"minimatch": "^5.1.0",
-				"resolve": "^1.22.1",
-				"unplugin": "^0.9.0"
-			}
-		},
-		"vite": {
-			"version": "3.0.9",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/vite/-/vite-3.0.9.tgz",
-			"integrity": "sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw==",
-			"dev": true,
-			"requires": {
-				"esbuild": "^0.14.47",
-				"fsevents": "~2.3.2",
-				"postcss": "^8.4.16",
-				"resolve": "^1.22.1",
-				"rollup": ">=2.75.6 <2.77.0 || ~2.77.0"
-			}
-		},
-		"vite-plugin-vue-setup-extend": {
-			"version": "0.4.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz",
-			"integrity": "sha512-WMbjPCui75fboFoUTHhdbXzu4Y/bJMv5N9QT9a7do3wNMNHHqrk+Tn2jrSJU0LS5fGl/EG+FEDBYVUeWIkDqXQ==",
-			"dev": true,
-			"requires": {
-				"@vue/compiler-sfc": "^3.2.29",
-				"magic-string": "^0.25.7"
-			},
-			"dependencies": {
-				"magic-string": {
-					"version": "0.25.9",
-					"resolved": "https://repo.huaweicloud.com/repository/npm/magic-string/-/magic-string-0.25.9.tgz",
-					"integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
-					"dev": true,
-					"requires": {
-						"sourcemap-codec": "^1.4.8"
-					}
-				}
-			}
-		},
-		"vue": {
-			"version": "3.2.37",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/vue/-/vue-3.2.37.tgz",
-			"integrity": "sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==",
-			"requires": {
-				"@vue/compiler-dom": "3.2.37",
-				"@vue/compiler-sfc": "3.2.37",
-				"@vue/runtime-dom": "3.2.37",
-				"@vue/server-renderer": "3.2.37",
-				"@vue/shared": "3.2.37"
-			}
-		},
-		"vue-cropperjs": {
-			"version": "5.0.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/vue-cropperjs/-/vue-cropperjs-5.0.0.tgz",
-			"integrity": "sha512-RhnC8O33uRZNkn74aiHZwNHnBJOXWlS4P6gsRI0lw4cZlWjKSCywZI9oSI9POlIPI6OYv30jvnHMXGch85tw7w==",
-			"requires": {
-				"cropperjs": "^1.5.6"
-			}
-		},
-		"vue-demi": {
-			"version": "0.13.8",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/vue-demi/-/vue-demi-0.13.8.tgz",
-			"integrity": "sha512-Vy1zbZhCOdsmvGR6tJhAvO5vhP7eiS8xkbYQSoVa7o6KlIy3W8Rc53ED4qI4qpeRDjv3mLfXSEpYU6Yq4pgXRg=="
-		},
-		"vue-router": {
-			"version": "4.1.3",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/vue-router/-/vue-router-4.1.3.tgz",
-			"integrity": "sha512-XvK81bcYglKiayT7/vYAg/f36ExPC4t90R/HIpzrZ5x+17BOWptXLCrEPufGgZeuq68ww4ekSIMBZY1qdUdfjA==",
-			"requires": {
-				"@vue/devtools-api": "^6.1.4"
-			}
-		},
-		"vue-schart": {
-			"version": "2.0.0",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/vue-schart/-/vue-schart-2.0.0.tgz",
-			"integrity": "sha512-qAu3e5wfMcq26wK1xeHExEWfGpnjfoN1R/9QXblNi+AsU/p52X7tTwhi+Fw7H/otfEufhEY2X7z7emaoF4QO+g==",
-			"requires": {
-				"schart.js": "^3.0.0"
-			}
-		},
-		"vue-tsc": {
-			"version": "0.38.9",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/vue-tsc/-/vue-tsc-0.38.9.tgz",
-			"integrity": "sha512-Yoy5phgvGqyF98Fb4mYqboR4Q149jrdcGv5kSmufXJUq++RZJ2iMVG0g6zl+v3t4ORVWkQmRpsV4x2szufZ0LQ==",
-			"dev": true,
-			"requires": {
-				"@volar/vue-typescript": "0.38.9"
-			}
-		},
-		"wangeditor": {
-			"version": "4.7.15",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/wangeditor/-/wangeditor-4.7.15.tgz",
-			"integrity": "sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==",
-			"requires": {
-				"@babel/runtime": "^7.11.2",
-				"@babel/runtime-corejs3": "^7.11.2",
-				"tslib": "^2.1.0"
-			}
-		},
-		"webpack-sources": {
-			"version": "3.2.3",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/webpack-sources/-/webpack-sources-3.2.3.tgz",
-			"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
-			"dev": true
-		},
-		"webpack-virtual-modules": {
-			"version": "0.4.4",
-			"resolved": "https://repo.huaweicloud.com/repository/npm/webpack-virtual-modules/-/webpack-virtual-modules-0.4.4.tgz",
-			"integrity": "sha512-h9atBP/bsZohWpHnr+2sic8Iecb60GxftXsWNLLLSqewgIsGzByd2gcIID4nXcG+3tNe4GQG3dLcff3kXupdRA==",
-			"dev": true
-		},
-		"wmf": {
-			"version": "1.0.2",
-			"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
-			"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="
-		},
-		"word": {
-			"version": "0.3.0",
-			"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
-			"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="
-		},
-		"xlsx": {
-			"version": "0.18.5",
-			"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
-			"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
-			"requires": {
-				"adler-32": "~1.3.0",
-				"cfb": "~1.2.1",
-				"codepage": "~1.15.0",
-				"crc-32": "~1.2.1",
-				"ssf": "~0.11.2",
-				"wmf": "~1.0.1",
-				"word": "~0.3.0"
-			}
-		}
-	}
-}

+ 44 - 40
package.json

@@ -1,40 +1,44 @@
-{
-	"name": "vue-manage-system",
-	"version": "5.3.5",
-	"private": true,
-	"scripts": {
-		"dev": "vite",
-		"build": "vue-tsc --noEmit && vite build",
-		"serve": "vite preview"
-	},
-	"dependencies": {
-		"@element-plus/icons-vue": "*",
-		"@wangeditor/editor": "^5.1.23",
-		"@wangeditor/editor-for-vue": "^5.1.12",
-		"axios": "^1.6.3",
-		"element-plus": "^2.4.4",
-		"md-editor-v3": "^2.11.2",
-		"nprogress": "^0.2.0",
-		"pinia": "^2.1.7",
-		"vue": "^3.3.0",
-		"vue-cropperjs": "^5.0.0",
-		"vue-router": "^4.2.5",
-		"vue-schart": "^2.0.0",
-		"xlsx": "^0.18.5"
-	},
-	"devDependencies": {
-		"@vitejs/plugin-vue": "^3.0.0",
-		"@vue/compiler-sfc": "^3.1.2",
-		"typescript": "^4.6.4",
-		"unplugin-auto-import": "^0.11.2",
-		"unplugin-vue-components": "^0.22.4",
-		"vite": "^3.0.0",
-		"vite-plugin-vue-setup-extend": "^0.4.0",
-		"vue-tsc": "^0.38.4"
-	},
-	"browserslist": [
-		"> 1%",
-		"last 2 versions",
-		"not dead"
-	]
-}
+{
+	"name": "vue-manage-system",
+	"version": "5.5.0",
+	"private": true,
+	"scripts": {
+		"dev": "vite",
+		"build": "vue-tsc --noEmit && vite build",
+		"serve": "vite preview"
+	},
+	"dependencies": {
+		"@element-plus/icons-vue": "*",
+		"@wangeditor/editor": "^5.1.23",
+		"@wangeditor/editor-for-vue": "^5.1.12",
+		"axios": "^1.6.3",
+		"countup.js": "^2.8.0",
+		"echarts": "^5.5.0",
+		"echarts-wordcloud": "^2.1.0",
+		"element-plus": "^2.6.3",
+		"md-editor-v3": "^2.11.2",
+		"nprogress": "^0.2.0",
+		"pinia": "^2.1.7",
+		"vue": "^3.4.5",
+		"vue-cropper": "1.1.1",
+		"vue-echarts": "^6.6.9",
+		"vue-router": "^4.2.5",
+		"vue-schart": "^2.0.0",
+		"xlsx": "^0.18.5"
+	},
+	"devDependencies": {
+		"@vitejs/plugin-vue": "^3.0.0",
+		"@vue/compiler-sfc": "^3.1.2",
+		"typescript": "^4.6.4",
+		"unplugin-auto-import": "^0.11.2",
+		"unplugin-vue-components": "^0.22.4",
+		"vite": "^3.0.0",
+		"vite-plugin-vue-setup-extend": "^0.4.0",
+		"vue-tsc": "^0.38.4"
+	},
+	"browserslist": [
+		"> 1%",
+		"last 2 versions",
+		"not dead"
+	]
+}

+ 46 - 0
public/mock/role.json

@@ -0,0 +1,46 @@
+{
+    "list": [
+        {
+            "id": 1,
+            "name": "管理员",
+            "key": "admin",
+            "status": true,
+            "permiss": [
+                "0",
+                "1",
+                "11",
+                "12",
+                "13",
+                "2",
+                "21",
+                "22",
+                "23",
+                "24",
+                "3",
+                "31",
+                "32",
+                "33",
+                "331",
+                "332",
+                "4",
+                "41",
+                "42",
+                "5"
+            ]
+        },
+        {
+            "id": 2,
+            "name": "普通用户",
+            "key": "user",
+            "status": true,
+            "permiss": [
+                "0",
+                "1",
+                "11",
+                "12",
+                "13"
+            ]
+        }
+    ],
+    "pageTotal": 2
+}

+ 40 - 39
public/table.json → public/mock/table.json

@@ -1,40 +1,41 @@
-{
-    "list": [{
-            "id": 1,
-            "name": "张三",
-            "money": 123,
-            "address": "广东省东莞市长安镇",
-            "state": "成功",
-            "date": "2019-11-1",
-            "thumb": "https://lin-xin.gitee.io/images/post/wms.png"
-        },
-        {
-            "id": 2,
-            "name": "李四",
-            "money": 456,
-            "address": "广东省广州市白云区",
-            "state": "成功",
-            "date": "2019-10-11",
-            "thumb": "https://lin-xin.gitee.io/images/post/node3.png"
-        },
-        {
-            "id": 3,
-            "name": "王五",
-            "money": 789,
-            "address": "湖南省长沙市",
-            "state": "失败",
-            "date": "2019-11-11",
-            "thumb": "https://lin-xin.gitee.io/images/post/parcel.png"
-        },
-        {
-            "id": 4,
-            "name": "赵六",
-            "money": 1011,
-            "address": "福建省厦门市鼓浪屿",
-            "state": "成功",
-            "date": "2019-10-20",
-            "thumb": "https://lin-xin.gitee.io/images/post/notice.png"
-        }
-    ],
-    "pageTotal": 4
+{
+    "list": [
+        {
+            "id": 1,
+            "name": "张三",
+            "money": 123,
+            "address": "广东省东莞市长安镇",
+            "state": true,
+            "date": "2019-11-1",
+            "thumb": "https://lin-xin.gitee.io/images/post/wms.png"
+        },
+        {
+            "id": 2,
+            "name": "李四",
+            "money": 456,
+            "address": "广东省广州市白云区",
+            "state": true,
+            "date": "2019-10-11",
+            "thumb": "https://lin-xin.gitee.io/images/post/node3.png"
+        },
+        {
+            "id": 3,
+            "name": "王五",
+            "money": 789,
+            "address": "湖南省长沙市",
+            "state": false,
+            "date": "2019-11-11",
+            "thumb": "https://lin-xin.gitee.io/images/post/parcel.png"
+        },
+        {
+            "id": 4,
+            "name": "赵六",
+            "money": 1011,
+            "address": "福建省厦门市鼓浪屿",
+            "state": true,
+            "date": "2019-10-20",
+            "thumb": "https://lin-xin.gitee.io/images/post/notice.png"
+        }
+    ],
+    "pageTotal": 4
 }

+ 23 - 0
public/mock/user.json

@@ -0,0 +1,23 @@
+{
+    "list": [
+        {
+            "id": 1,
+            "name": "张三",
+            "password": "123",
+            "email": "123@qq.com",
+            "phone": "12345678944",
+            "date": "2024-01-01",
+            "role": "管理员"
+        },
+        {
+            "id": 2,
+            "name": "李四",
+            "password": "123",
+            "email": "1234@qq.com",
+            "phone": "12345678945",
+            "date": "2024-01-01",
+            "role": "普通用户"
+        }
+    ],
+    "pageTotal": 2
+}

BIN
screenshots/wms1.png


BIN
screenshots/wms3.png


+ 17 - 14
src/App.vue

@@ -1,14 +1,17 @@
-<template>
-	<el-config-provider :locale="zhCn">
-		<router-view />
-	</el-config-provider>
-</template>
-
-<script setup lang="ts">
-import { ElConfigProvider } from 'element-plus';
-import zhCn from 'element-plus/es/locale/lang/zh-cn';
-</script>
-<style>
-@import './assets/css/main.css';
-@import './assets/css/color-dark.scss';
-</style>
+<template>
+	<el-config-provider :locale="zhCn">
+		<router-view />
+	</el-config-provider>
+</template>
+
+<script setup lang="ts">
+import { ElConfigProvider } from 'element-plus';
+import zhCn from 'element-plus/es/locale/lang/zh-cn';
+import { useThemeStore } from './store/theme';
+
+const theme = useThemeStore();
+theme.initTheme();
+</script>
+<style>
+@import './assets/css/main.css';
+</style>

+ 22 - 8
src/api/index.ts

@@ -1,8 +1,22 @@
-import request from '../utils/request';
-
-export const fetchData = () => {
-    return request({
-        url: 'https://www.fastmock.site/mock/dc695d037038802def4b989ba4650c3f/vms/getUser',
-        method: 'post'
-    });
-};
+import request from '../utils/request';
+
+export const fetchData = () => {
+    return request({
+        url: './mock/table.json',
+        method: 'get'
+    });
+};
+
+export const fetchUserData = () => {
+    return request({
+        url: './mock/user.json',
+        method: 'get'
+    });
+};
+
+export const fetchRoleData = () => {
+    return request({
+        url: './mock/role.json',
+        method: 'get'
+    });
+};

+ 0 - 23
src/assets/css/color-dark.scss

@@ -1,23 +0,0 @@
-.header{
-    background-color: #242f42;
-}
-.login-wrap{
-    background: #324157;
-}
-.plugins-tips{
-    background: #eef1f6;
-}
-.plugins-tips a{
-    color: var(--el-color-primary);
-}
-
-.tags-li.active {
-    border: 1px solid var(--el-color-primary);
-    background-color: var(--el-color-primary);
-}
-.message-title{
-    color: var(--el-color-primary);
-}
-.collapse-btn:hover{
-    background: rgb(40,52,70);
-}

+ 95 - 132
src/assets/css/main.css

@@ -1,132 +1,95 @@
-* {
-    margin: 0;
-    padding: 0;
-}
-
-html,
-body,
-#app,
-.wrapper {
-    width: 100%;
-    height: 100%;
-    overflow: hidden;
-}
-
-body {
-    font-family: 'PingFang SC', "Helvetica Neue", Helvetica, "microsoft yahei", arial, STHeiTi, sans-serif;
-}
-
-a {
-    text-decoration: none
-}
-
-
-.content-box {
-    position: absolute;
-    left: 250px;
-    right: 0;
-    top: 70px;
-    bottom: 0;
-    padding-bottom: 30px;
-    -webkit-transition: left .3s ease-in-out;
-    transition: left .3s ease-in-out;
-    background: #f0f0f0;
-}
-
-.content {
-    width: auto;
-    height: 100%;
-    padding: 10px;
-    overflow-y: scroll;
-    box-sizing: border-box;
-}
-
-.content-collapse {
-    left: 65px;
-}
-
-.container {
-    padding: 30px;
-    background: #fff;
-    border: 1px solid #ddd;
-    border-radius: 5px;
-}
-
-.crumbs {
-    margin: 10px 0;
-}
-
-.el-table th {
-    background-color: #f5f7fa !important;
-}
-
-.pagination {
-    margin: 20px 0;
-    text-align: right;
-}
-
-.plugins-tips {
-    padding: 20px 10px;
-    margin-bottom: 20px;
-}
-
-.el-button+.el-tooltip {
-    margin-left: 10px;
-}
-
-.mgb20 {
-    margin-bottom: 20px;
-}
-
-.move-enter-active,
-.move-leave-active {
-    transition: opacity .1s ease;
-}
-
-.move-enter-from,
-.move-leave-to {
-    opacity: 0;
-}
-
-/*BaseForm*/
-
-.form-box {
-    width: 600px;
-}
-
-.form-box .line {
-    text-align: center;
-}
-
-.el-time-panel__content::after,
-.el-time-panel__content::before {
-    margin-top: -7px;
-}
-
-.el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) {
-    padding-bottom: 0;
-}
-
-
-[class*=" el-icon-"], [class^=el-icon-] {
-    font-style: normal;
-    font-weight: 400;
-    font-variant: normal;
-    text-transform: none;
-    line-height: 1;
-    vertical-align: baseline;
-    display: inline-block;
-    -webkit-font-smoothing: antialiased;
-    -moz-osx-font-smoothing: grayscale;
-}
-.el-sub-menu [class^=el-icon-] {
-    vertical-align: middle;
-    margin-right: 5px;
-    width: 24px;
-    text-align: center;
-    font-size: 18px;
-}
-
-[hidden]{
-    display: none !important;
-}
+* {
+	margin: 0;
+	padding: 0;
+	outline: 0 !important;
+}
+
+html,
+body,
+#app,
+.wrapper {
+	width: 100%;
+	height: 100%;
+	overflow: hidden;
+}
+
+body {
+	font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, 'microsoft yahei', arial, STHeiTi, sans-serif;
+}
+
+a {
+	text-decoration: none;
+}
+i {
+	font-style: normal;
+}
+
+.container {
+	padding: 30px;
+	background: #fff;
+	border: 1px solid #ddd;
+	border-radius: 5px;
+}
+
+.el-table th {
+	background-color: #f5f7fa !important;
+}
+
+.plugins-tips {
+	padding: 20px 10px;
+	margin-bottom: 20px;
+	background: #eef1f6;
+}
+
+.plugins-tips a {
+	color: var(--el-color-primary);
+}
+
+.el-button + .el-tooltip {
+	margin-left: 10px;
+}
+
+.mgb20 {
+	margin-bottom: 20px;
+}
+.mgb10 {
+	margin-bottom: 10px;
+}
+.mr10 {
+	margin-right: 10px;
+}
+
+.move-enter-active,
+.move-leave-active {
+	transition: opacity 0.1s ease;
+}
+
+.move-enter-from,
+.move-leave-to {
+	opacity: 0;
+}
+
+.el-time-panel__content::after,
+.el-time-panel__content::before {
+	margin-top: -7px;
+}
+
+.el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) {
+	padding-bottom: 0;
+}
+
+[hidden] {
+	display: none !important;
+}
+
+.flex-center {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+
+:root {
+	--header-bg-color: #242f42;
+	--header-text-color: #fff;
+	--active-color: var(--el-color-primary);
+}

+ 1 - 0
src/assets/img/logo.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144.08 128.61"><title>&#x8D44;&#x6E90; 82</title><path d="M72.23 128.61c-7.1-.23-11.51-3.72-14.76-9.36C48 102.87 38.43 86.59 29.1 70.16a36 36 0 0 1-4.47-11.35A14.61 14.61 0 0 1 34 42.51c7.49-2.71 15.71-.21 19.67 6.43 7.52 12.56 14.77 25.27 22.12 37.92 3 5.17 5.89 10.43 9 15.51 5 8 3.45 18-4.22 23.31-2.3 1.62-5.52 1.99-8.34 2.93z" fill="#2ef2e9"/><path d="M72.66.33c6-.57 10.39 2.6 13.51 8C95.61 24.69 105 41.1 114.52 57.4c3.9 6.65-.28 17.13-6.39 20.44-8.93 4.83-17.88 1.28-21.86-5.62C76.82 55.86 67.14 39.62 58.11 23 52.06 12 59.61.24 72.66.33z" fill="#fa6663"/><path d="M144.08 15.83c-.58 8.62-6.73 15.57-15.51 15.66-9.31.09-16.87-7-16.95-15.62S119 0 127.87 0c9.13.09 16.22 7 16.21 15.83z" fill="#fbb355"/><path d="M16.24 31.5C7 31.33-.19 24.42 0 15.8.19 7.5 7.19-.06 14.64 0c10.53.08 18.27 6.73 17.61 15.9-.64 8.96-6.25 15.28-16.01 15.6z" fill="#8a56c2"/></svg>

BIN
src/assets/img/ucenter-bg.jpg


+ 39 - 0
src/components/countup.vue

@@ -0,0 +1,39 @@
+<template>
+    <span ref="countRef"></span>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref, watch } from 'vue';
+import { CountUp } from 'countup.js';
+
+const props = defineProps({
+    end: {
+        type: Number,
+        required: true,
+    },
+    options: {
+        type: Object,
+        default: () => ({}),
+        required: false,
+    },
+});
+
+const countRef = ref<any>(null);
+let countUp: any;
+onMounted(() => {
+    countUp = new CountUp(countRef.value, props.end, props.options);
+    if (countUp.error) {
+        console.error(countUp.error);
+        return;
+    }
+    countUp.start();
+});
+
+watch(() => props.end, (newVal) => {
+    if (countUp) {
+        countUp.update(newVal);
+    }
+});
+
+
+</script>

+ 200 - 154
src/components/header.vue

@@ -1,154 +1,200 @@
-<template>
-	<div class="header">
-		<!-- 折叠按钮 -->
-		<div class="collapse-btn" @click="collapseChage">
-			<el-icon v-if="sidebar.collapse"><Expand /></el-icon>
-			<el-icon v-else><Fold /></el-icon>
-		</div>
-		<div class="logo">后台管理系统</div>
-		<div class="header-right">
-			<div class="header-user-con">
-				<!-- 消息中心 -->
-				<div class="btn-bell" @click="router.push('/tabs')">
-					<el-tooltip
-						effect="dark"
-						:content="message ? `有${message}条未读消息` : `消息中心`"
-						placement="bottom"
-					>
-						<i class="el-icon-lx-notice"></i>
-					</el-tooltip>
-					<span class="btn-bell-badge" v-if="message"></span>
-				</div>
-				<!-- 用户头像 -->
-				<el-avatar class="user-avator" :size="30" :src="imgurl" />
-				<!-- 用户名下拉菜单 -->
-				<el-dropdown class="user-name" trigger="click" @command="handleCommand">
-					<span class="el-dropdown-link">
-						{{ username }}
-						<el-icon class="el-icon--right">
-							<arrow-down />
-						</el-icon>
-					</span>
-					<template #dropdown>
-						<el-dropdown-menu>
-							<a href="https://github.com/lin-xin/vue-manage-system" target="_blank">
-								<el-dropdown-item>项目仓库</el-dropdown-item>
-							</a>
-							<el-dropdown-item command="user">个人中心</el-dropdown-item>
-							<el-dropdown-item divided command="loginout">退出登录</el-dropdown-item>
-						</el-dropdown-menu>
-					</template>
-				</el-dropdown>
-			</div>
-		</div>
-	</div>
-</template>
-<script setup lang="ts">
-import { onMounted } from 'vue';
-import { useSidebarStore } from '../store/sidebar';
-import { useRouter } from 'vue-router';
-import imgurl from '../assets/img/img.jpg';
-
-const username: string | null = localStorage.getItem('ms_username');
-const message: number = 2;
-
-const sidebar = useSidebarStore();
-// 侧边栏折叠
-const collapseChage = () => {
-	sidebar.handleCollapse();
-};
-
-onMounted(() => {
-	if (document.body.clientWidth < 1500) {
-		collapseChage();
-	}
-});
-
-// 用户名下拉菜单选择事件
-const router = useRouter();
-const handleCommand = (command: string) => {
-	if (command == 'loginout') {
-		localStorage.removeItem('ms_username');
-		router.push('/login');
-	} else if (command == 'user') {
-		router.push('/user');
-	}
-};
-</script>
-<style scoped>
-.header {
-	position: relative;
-	box-sizing: border-box;
-	width: 100%;
-	height: 70px;
-	font-size: 22px;
-	color: #fff;
-}
-.collapse-btn {
-	display: flex;
-	justify-content: center;
-	align-items: center;
-	height: 100%;
-	float: left;
-	padding: 0 21px;
-	cursor: pointer;
-}
-.header .logo {
-	float: left;
-	width: 250px;
-	line-height: 70px;
-}
-.header-right {
-	float: right;
-	padding-right: 50px;
-}
-.header-user-con {
-	display: flex;
-	height: 70px;
-	align-items: center;
-}
-.btn-fullscreen {
-	transform: rotate(45deg);
-	margin-right: 5px;
-	font-size: 24px;
-}
-.btn-bell,
-.btn-fullscreen {
-	position: relative;
-	width: 30px;
-	height: 30px;
-	text-align: center;
-	border-radius: 15px;
-	cursor: pointer;
-	display: flex;
-	align-items: center;
-}
-.btn-bell-badge {
-	position: absolute;
-	right: 4px;
-	top: 0px;
-	width: 8px;
-	height: 8px;
-	border-radius: 4px;
-	background: #f56c6c;
-	color: #fff;
-}
-.btn-bell .el-icon-lx-notice {
-	color: #fff;
-}
-.user-name {
-	margin-left: 10px;
-}
-.user-avator {
-	margin-left: 20px;
-}
-.el-dropdown-link {
-	color: #fff;
-	cursor: pointer;
-	display: flex;
-	align-items: center;
-}
-.el-dropdown-menu__item {
-	text-align: center;
-}
-</style>
+<template>
+	<div class="header">
+		<!-- 折叠按钮 -->
+		<div class="header-left">
+			<img class="logo" src="../assets/img/logo.svg" alt="">
+			<div class="web-title">
+				后台管理系统
+			</div>
+			<div class="collapse-btn" @click="collapseChage">
+				<el-icon v-if="sidebar.collapse">
+					<Expand />
+				</el-icon>
+				<el-icon v-else>
+					<Fold />
+				</el-icon>
+			</div>
+		</div>
+		<div class="header-right">
+			<div class="header-user-con">
+				<div class="btn-icon" @click="router.push('/theme')">
+					<el-tooltip effect="dark" content="设置主题" placement="bottom">
+						<i class="el-icon-lx-skin"></i>
+					</el-tooltip>
+				</div>
+				<div class="btn-icon" @click="router.push('/ucenter')">
+					<el-tooltip effect="dark" :content="message ? `有${message}条未读消息` : `消息中心`" placement="bottom">
+						<i class="el-icon-lx-notice"></i>
+					</el-tooltip>
+					<span class="btn-bell-badge" v-if="message"></span>
+				</div>
+				<div class="btn-icon" @click="setFullScreen">
+					<el-tooltip effect="dark" content="全屏" placement="bottom">
+						<i class="el-icon-lx-full"></i>
+					</el-tooltip>
+				</div>
+				<!-- 用户头像 -->
+				<el-avatar class="user-avator" :size="30" :src="imgurl" />
+				<!-- 用户名下拉菜单 -->
+				<el-dropdown class="user-name" trigger="click" @command="handleCommand">
+					<span class="el-dropdown-link">
+						{{ username }}
+						<el-icon class="el-icon--right">
+							<arrow-down />
+						</el-icon>
+					</span>
+					<template #dropdown>
+						<el-dropdown-menu>
+							<a href="https://github.com/lin-xin/vue-manage-system" target="_blank">
+								<el-dropdown-item>项目仓库</el-dropdown-item>
+							</a>
+							<el-dropdown-item command="user">个人中心</el-dropdown-item>
+							<el-dropdown-item divided command="loginout">退出登录</el-dropdown-item>
+						</el-dropdown-menu>
+					</template>
+				</el-dropdown>
+			</div>
+		</div>
+	</div>
+</template>
+<script setup lang="ts">
+import { onMounted } from 'vue';
+import { useSidebarStore } from '../store/sidebar';
+import { useRouter } from 'vue-router';
+import imgurl from '../assets/img/img.jpg';
+
+const username: string | null = localStorage.getItem('ms_username');
+const message: number = 2;
+
+const sidebar = useSidebarStore();
+// 侧边栏折叠
+const collapseChage = () => {
+	sidebar.handleCollapse();
+};
+
+onMounted(() => {
+	if (document.body.clientWidth < 1500) {
+		collapseChage();
+	}
+});
+
+// 用户名下拉菜单选择事件
+const router = useRouter();
+const handleCommand = (command: string) => {
+	if (command == 'loginout') {
+		localStorage.removeItem('ms_username');
+		router.push('/login');
+	} else if (command == 'user') {
+		router.push('/ucenter');
+	}
+};
+
+const setFullScreen = () => {
+	if (document.fullscreenElement) {
+		document.exitFullscreen();
+	} else {
+		document.body.requestFullscreen.call(document.body);
+	}
+}
+</script>
+<style scoped>
+.header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	box-sizing: border-box;
+	width: 100%;
+	height: 70px;
+	color: var(--header-text-color);
+	background-color: var(--header-bg-color);
+	border-bottom: 1px solid #ddd;
+}
+
+.header-left {
+	display: flex;
+	align-items: center;
+	padding-left: 20px;
+	height: 100%;
+}
+
+.logo {
+	width: 35px;
+}
+
+.web-title {
+	margin: 0 40px 0 10px;
+	font-size: 22px;
+}
+
+.collapse-btn {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	height: 100%;
+	padding: 0 10px;
+	cursor: pointer;
+	opacity: 0.8;
+	font-size: 22px;
+}
+
+.collapse-btn:hover {
+	opacity: 1;
+}
+
+.header-right {
+	float: right;
+	padding-right: 50px;
+}
+
+.header-user-con {
+	display: flex;
+	height: 70px;
+	align-items: center;
+}
+
+.btn-fullscreen {
+	transform: rotate(45deg);
+	margin-right: 5px;
+	font-size: 24px;
+}
+
+.btn-icon {
+	position: relative;
+	width: 30px;
+	height: 30px;
+	text-align: center;
+	cursor: pointer;
+	display: flex;
+	align-items: center;
+	color: var(--header-text-color);
+	margin: 0 5px;
+	font-size: 20px;
+}
+
+.btn-bell-badge {
+	position: absolute;
+	right: 4px;
+	top: 0px;
+	width: 8px;
+	height: 8px;
+	border-radius: 4px;
+	background: #f56c6c;
+	color: var(--header-text-color);
+}
+
+
+.user-avator {
+	margin: 0 10px 0 20px;
+}
+
+.el-dropdown-link {
+	color: var(--header-text-color);
+	cursor: pointer;
+	display: flex;
+	align-items: center;
+}
+
+.el-dropdown-menu__item {
+	text-align: center;
+}
+</style>

+ 221 - 0
src/components/menu.ts

@@ -0,0 +1,221 @@
+import { Menus } from '@/types/menu';
+
+export const menuData: Menus[] = [
+    {
+        id: '0',
+        title: '系统首页',
+        index: '/dashboard',
+        icon: 'Odometer',
+    },
+    {
+        id: '1',
+        title: '系统管理',
+        index: '1',
+        icon: 'HomeFilled',
+        children: [
+            {
+                id: '11',
+                pid: '1',
+                index: '/system-user',
+                title: '用户管理',
+            },
+            {
+                id: '12',
+                pid: '1',
+                index: '/system-role',
+                title: '角色管理',
+            },
+            {
+                id: '13',
+                pid: '1',
+                index: '/system-menu',
+                title: '菜单管理',
+            },
+        ],
+    },
+    {
+        id: '2',
+        title: '组件',
+        index: '2-1',
+        icon: 'Calendar',
+        children: [
+            {
+                id: '21',
+                pid: '3',
+                index: '/form',
+                title: '表单',
+            },
+            {
+                id: '22',
+                pid: '3',
+                index: '/upload',
+                title: '上传',
+            },
+            {
+                id: '23',
+                pid: '2',
+                index: '/carousel',
+                title: '走马灯',
+            },
+            {
+                id: '24',
+                pid: '2',
+                index: '/calendar',
+                title: '日历',
+            },
+            {
+                id: '25',
+                pid: '2',
+                index: '/watermark',
+                title: '水印',
+            },
+            {
+                id: '26',
+                pid: '2',
+                index: '/tour',
+                title: '分布引导',
+            },
+            {
+                id: '27',
+                pid: '2',
+                index: '/steps',
+                title: '步骤条',
+            },
+            {
+                id: '28',
+                pid: '2',
+                index: '/statistic',
+                title: '统计',
+            },
+            {
+                id: '29',
+                pid: '3',
+                index: '29',
+                title: '三级菜单',
+                children: [
+                    {
+                        id: '291',
+                        pid: '29',
+                        index: '/editor',
+                        title: '富文本编辑器',
+                    },
+                    {
+                        id: '292',
+                        pid: '29',
+                        index: '/markdown',
+                        title: 'markdown编辑器',
+                    },
+                ],
+            },
+        ],
+    },
+    {
+        id: '3',
+        title: '表格',
+        index: '3',
+        icon: 'Calendar',
+        children: [
+            {
+                id: '31',
+                pid: '3',
+                index: '/table',
+                title: '基础表格',
+            },
+            {
+                id: '32',
+                pid: '3',
+                index: '/table-editor',
+                title: '可编辑表格',
+            },
+            {
+                id: '33',
+                pid: '3',
+                index: '/import',
+                title: '导入Excel',
+            },
+            {
+                id: '34',
+                pid: '3',
+                index: '/export',
+                title: '导出Excel',
+            },
+        ],
+    },
+    {
+        id: '4',
+        icon: 'PieChart',
+        index: '4',
+        title: '图表',
+        children: [
+            {
+                id: '41',
+                pid: '4',
+                index: '/schart',
+                title: 'schart图表',
+            },
+            {
+                id: '42',
+                pid: '4',
+                index: '/echarts',
+                title: 'echarts图表',
+            },
+        ],
+    },
+    {
+        id: '5',
+        icon: 'Guide',
+        index: '/icon',
+        title: '图标',
+        permiss: '5',
+    },
+    {
+        id: '7',
+        icon: 'Brush',
+        index: '/theme',
+        title: '主题',
+    },
+    {
+        id: '6',
+        icon: 'DocumentAdd',
+        index: '6',
+        title: '附加页面',
+        children: [
+            {
+                id: '61',
+                pid: '6',
+                index: '/ucenter',
+                title: '个人中心',
+            },
+            {
+                id: '62',
+                pid: '6',
+                index: '/login',
+                title: '登录',
+            },
+            {
+                id: '63',
+                pid: '6',
+                index: '/register',
+                title: '注册',
+            },
+            {
+                id: '64',
+                pid: '6',
+                index: '/reset-pwd',
+                title: '重设密码',
+            },
+            {
+                id: '65',
+                pid: '6',
+                index: '/403',
+                title: '403',
+            },
+            {
+                id: '66',
+                pid: '6',
+                index: '/404',
+                title: '404',
+            },
+        ],
+    },
+];

+ 90 - 181
src/components/sidebar.vue

@@ -1,181 +1,90 @@
-<template>
-    <div class="sidebar">
-        <el-menu
-            class="sidebar-el-menu"
-            :default-active="onRoutes"
-            :collapse="sidebar.collapse"
-            background-color="#324157"
-            text-color="#bfcbd9"
-            active-text-color="#20a0ff"
-            unique-opened
-            router
-        >
-            <template v-for="item in items">
-                <template v-if="item.subs">
-                    <el-sub-menu :index="item.index" :key="item.index" v-permiss="item.permiss">
-                        <template #title>
-                            <el-icon>
-                                <component :is="item.icon"></component>
-                            </el-icon>
-                            <span>{{ item.title }}</span>
-                        </template>
-                        <template v-for="subItem in item.subs">
-                            <el-sub-menu
-                                v-if="subItem.subs"
-                                :index="subItem.index"
-                                :key="subItem.index"
-                                v-permiss="item.permiss"
-                            >
-                                <template #title>{{ subItem.title }}</template>
-                                <el-menu-item v-for="(threeItem, i) in subItem.subs" :key="i" :index="threeItem.index">
-                                    {{ threeItem.title }}
-                                </el-menu-item>
-                            </el-sub-menu>
-                            <el-menu-item v-else :index="subItem.index" v-permiss="item.permiss">
-                                {{ subItem.title }}
-                            </el-menu-item>
-                        </template>
-                    </el-sub-menu>
-                </template>
-                <template v-else>
-                    <el-menu-item :index="item.index" :key="item.index" v-permiss="item.permiss">
-                        <el-icon>
-                            <component :is="item.icon"></component>
-                        </el-icon>
-                        <template #title>{{ item.title }}</template>
-                    </el-menu-item>
-                </template>
-            </template>
-        </el-menu>
-    </div>
-</template>
-
-<script setup lang="ts">
-import { computed } from 'vue';
-import { useSidebarStore } from '../store/sidebar';
-import { useRoute } from 'vue-router';
-
-const items = [
-    {
-        icon: 'Odometer',
-        index: '/dashboard',
-        title: '系统首页',
-        permiss: '1',
-    },
-    {
-        icon: 'Calendar',
-        index: '1',
-        title: '表格相关',
-        permiss: '2',
-        subs: [
-            {
-                index: '/table',
-                title: '常用表格',
-                permiss: '2',
-            },
-            {
-                index: '/import',
-                title: '导入Excel',
-                permiss: '2',
-            },
-            {
-                index: '/export',
-                title: '导出Excel',
-                permiss: '2',
-            },
-        ],
-    },
-    {
-        icon: 'DocumentCopy',
-        index: '/tabs',
-        title: 'tab选项卡',
-        permiss: '3',
-    },
-    {
-        icon: 'Edit',
-        index: '3',
-        title: '表单相关',
-        permiss: '4',
-        subs: [
-            {
-                index: '/form',
-                title: '基本表单',
-                permiss: '5',
-            },
-            {
-                index: '/upload',
-                title: '文件上传',
-                permiss: '6',
-            },
-            {
-                index: '4',
-                title: '三级菜单',
-                permiss: '7',
-                subs: [
-                    {
-                        index: '/editor',
-                        title: '富文本编辑器',
-                        permiss: '8',
-                    },
-                    {
-                        index: '/markdown',
-                        title: 'markdown编辑器',
-                        permiss: '9',
-                    },
-                ],
-            },
-        ],
-    },
-    {
-        icon: 'Setting',
-        index: '/icon',
-        title: '自定义图标',
-        permiss: '10',
-    },
-    {
-        icon: 'PieChart',
-        index: '/charts',
-        title: 'schart图表',
-        permiss: '11',
-    },
-    {
-        icon: 'Warning',
-        index: '/permission',
-        title: '权限管理',
-        permiss: '13',
-    },
-    {
-        icon: 'CoffeeCup',
-        index: '/donate',
-        title: '支持作者',
-        permiss: '14',
-    },
-];
-
-const route = useRoute();
-const onRoutes = computed(() => {
-    return route.path;
-});
-
-const sidebar = useSidebarStore();
-</script>
-
-<style scoped>
-.sidebar {
-    display: block;
-    position: absolute;
-    left: 0;
-    top: 70px;
-    bottom: 0;
-    overflow-y: scroll;
-}
-.sidebar::-webkit-scrollbar {
-    width: 0;
-}
-.sidebar-el-menu:not(.el-menu--collapse) {
-    width: 250px;
-}
-.sidebar > ul {
-    height: 100%;
-}
-</style>
+<template>
+    <div class="sidebar">
+        <el-menu
+            class="sidebar-el-menu"
+            :default-active="onRoutes"
+            :collapse="sidebar.collapse"
+            :background-color="sidebar.bgColor"
+            :text-color="sidebar.textColor"
+            router
+        >
+            <template v-for="item in menuData">
+                <template v-if="item.children">
+                    <el-sub-menu :index="item.index" :key="item.index" v-permiss="item.id">
+                        <template #title>
+                            <el-icon>
+                                <component :is="item.icon"></component>
+                            </el-icon>
+                            <span>{{ item.title }}</span>
+                        </template>
+                        <template v-for="subItem in item.children">
+                            <el-sub-menu
+                                v-if="subItem.children"
+                                :index="subItem.index"
+                                :key="subItem.index"
+                                v-permiss="item.id"
+                            >
+                                <template #title>{{ subItem.title }}</template>
+                                <el-menu-item
+                                    v-for="(threeItem, i) in subItem.children"
+                                    :key="i"
+                                    :index="threeItem.index"
+                                >
+                                    {{ threeItem.title }}
+                                </el-menu-item>
+                            </el-sub-menu>
+                            <el-menu-item v-else :index="subItem.index" v-permiss="item.id">
+                                {{ subItem.title }}
+                            </el-menu-item>
+                        </template>
+                    </el-sub-menu>
+                </template>
+                <template v-else>
+                    <el-menu-item :index="item.index" :key="item.index" v-permiss="item.id">
+                        <el-icon>
+                            <component :is="item.icon"></component>
+                        </el-icon>
+                        <template #title>{{ item.title }}</template>
+                    </el-menu-item>
+                </template>
+            </template>
+        </el-menu>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import { useSidebarStore } from '../store/sidebar';
+import { useRoute } from 'vue-router';
+import { menuData } from '@/components/menu';
+
+const route = useRoute();
+const onRoutes = computed(() => {
+    return route.path;
+});
+
+const sidebar = useSidebarStore();
+</script>
+
+<style scoped>
+.sidebar {
+    display: block;
+    position: absolute;
+    left: 0;
+    top: 70px;
+    bottom: 0;
+    overflow-y: scroll;
+}
+
+.sidebar::-webkit-scrollbar {
+    width: 0;
+}
+
+.sidebar-el-menu:not(.el-menu--collapse) {
+    width: 250px;
+}
+
+.sidebar-el-menu {
+    min-height: 100%;
+}
+</style>

+ 211 - 0
src/components/table-custom.vue

@@ -0,0 +1,211 @@
+<template>
+    <div>
+        <div class="table-toolbar" v-if="hasToolbar">
+            <div class="table-toolbar-left">
+                <slot name="toolbarBtn"></slot>
+            </div>
+            <div class="table-toolbar-right flex-center">
+                <template v-if="multipleSelection.length > 0">
+                    <el-tooltip effect="dark" content="删除选中" placement="top">
+                        <el-icon class="columns-setting-icon" @click="delSelection(multipleSelection)">
+                            <Delete />
+                        </el-icon>
+                    </el-tooltip>
+                    <el-divider direction="vertical" />
+                </template>
+                <el-tooltip effect="dark" content="刷新" placement="top">
+                    <el-icon class="columns-setting-icon" @click="refresh">
+                        <Refresh />
+                    </el-icon>
+                </el-tooltip>
+                <el-divider direction="vertical" />
+                <el-tooltip effect="dark" content="列设置" placement="top">
+                    <el-dropdown :hide-on-click="false" size="small" trigger="click">
+                        <el-icon class="columns-setting-icon">
+                            <Setting />
+                        </el-icon>
+                        <template #dropdown>
+                            <el-dropdown-menu>
+                                <el-dropdown-item v-for="c in columns">
+                                    <el-checkbox v-model="c.visible" :label="c.label" />
+                                </el-dropdown-item>
+                            </el-dropdown-menu>
+                        </template>
+                    </el-dropdown>
+                </el-tooltip>
+            </div>
+        </div>
+        <el-table class="mgb20" :style="{ width: '100%' }" border :data="tableData" :row-key="rowKey"
+            @selection-change="handleSelectionChange" table-layout="auto">
+            <template v-for="item in columns" :key="item.prop">
+                <el-table-column v-if="item.visible" :prop="item.prop" :label="item.label" :width="item.width"
+                    :type="item.type" :align="item.align || 'center'">
+
+                    <template #default="{ row, column, $index }" v-if="item.type === 'index'">
+                        {{ getIndex($index) }}
+                    </template>
+                    <template #default="{ row, column, $index }" v-if="!item.type">
+                        <slot :name="item.prop" :rows="row" :index="$index">
+                            <template v-if="item.prop == 'operator'">
+                                <el-button type="warning" size="small" :icon="View" @click="viewFunc(row)">
+                                    查看
+                                </el-button>
+                                <el-button type="primary" size="small" :icon="Edit" @click="editFunc(row)">
+                                    编辑
+                                </el-button>
+                                <el-button type="danger" size="small" :icon="Delete" @click="handleDelete(row)">
+                                    删除
+                                </el-button>
+                            </template>
+                            <span v-else-if="item.formatter">
+                                {{ item.formatter(row[item.prop]) }}
+                            </span>
+                            <span v-else>
+                                {{ row[item.prop] }}
+                            </span>
+                        </slot>
+                    </template>
+                </el-table-column>
+            </template>
+        </el-table>
+        <el-pagination v-if="hasPagination" :current-page="currentPage" :page-size="pageSize" :background="true"
+            :layout="layout" :total="total" @current-change="handleCurrentChange" />
+    </div>
+</template>
+
+<script setup lang="ts">
+import { toRefs, PropType, ref } from 'vue'
+import { Delete, Edit, View, Refresh } from '@element-plus/icons-vue';
+import { ElMessageBox } from 'element-plus';
+
+const props = defineProps({
+    // 表格相关
+    tableData: {
+        type: Array,
+        default: []
+    },
+    columns: {
+        type: Array as PropType<any[]>,
+        default: []
+    },
+    rowKey: {
+        type: String,
+        default: 'id'
+    },
+    hasToolbar: {
+        type: Boolean,
+        default: true
+    },
+    //  分页相关
+    hasPagination: {
+        type: Boolean,
+        default: true
+    },
+    total: {
+        type: Number,
+        default: 0
+    },
+    currentPage: {
+        type: Number,
+        default: 1
+    },
+    pageSize: {
+        type: Number,
+        default: 10
+    },
+
+    layout: {
+        type: String,
+        default: 'total, prev, pager, next'
+    },
+    delFunc: {
+        type: Function,
+        default: () => { }
+    },
+    viewFunc: {
+        type: Function,
+        default: () => { }
+    },
+    editFunc: {
+        type: Function,
+        default: () => { }
+    },
+    delSelection: {
+        type: Function,
+        default: () => { }
+    },
+    refresh: {
+        type: Function,
+        default: () => { }
+    },
+    changePage: {
+        type: Function,
+        default: () => { }
+    }
+})
+
+let {
+    tableData,
+    columns,
+    rowKey,
+    hasToolbar,
+    hasPagination,
+    total,
+    currentPage,
+    pageSize,
+    layout,
+} = toRefs(props)
+
+columns.value.forEach((item) => {
+    if (item.visible === undefined) {
+        item.visible = true
+    }
+})
+
+// 当选择项发生变化时会触发该事件
+const multipleSelection = ref([])
+const handleSelectionChange = (selection: any[]) => {
+    multipleSelection.value = selection
+}
+
+// 当前页码变化的事件
+const handleCurrentChange = (val: number) => {
+    props.changePage(val)
+}
+
+const handleDelete = (row) => {
+    ElMessageBox.confirm('确定要删除吗?', '提示', {
+        type: 'warning'
+    })
+        .then(async () => {
+            props.delFunc(row);
+        })
+        .catch(() => { });
+};
+
+const getIndex = (index: number) => {
+    return index + 1 + (currentPage.value - 1) * pageSize.value
+}
+
+</script>
+
+<style scoped>
+.table-toolbar {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-end;
+    margin-bottom: 10px;
+}
+
+.columns-setting-icon {
+    display: block;
+    font-size: 18px;
+    cursor: pointer;
+    color: #676767;
+}
+</style>
+<style>
+.table-header .cell {
+    color: #333;
+}
+</style>

+ 9 - 29
src/components/table-detail.vue

@@ -1,32 +1,10 @@
 <template>
-	<el-descriptions title="" :column="2" border>
-		<el-descriptions-item>
-			<template #label> 用户ID </template>
-			{{ data.id }}
-		</el-descriptions-item>
-		<el-descriptions-item>
-			<template #label> 用户名 </template>
-			{{ data.name }}
-		</el-descriptions-item>
-		<el-descriptions-item>
-			<template #label> 账户余额 </template>
-			{{ data.money }}
-		</el-descriptions-item>
-		<el-descriptions-item>
-			<template #label> 账户状态 </template>
-			{{ data.state ? '正常' : '异常' }}
-		</el-descriptions-item>
-		<el-descriptions-item :span="2">
-			<template #label> 地址 </template>
-			{{ data.address }}
-		</el-descriptions-item>
-		<el-descriptions-item>
-			<template #label> 日期 </template>
-			{{ data.date }}
-		</el-descriptions-item>
-		<el-descriptions-item>
-			<template #label> 头像 </template>
-			<img :src="data.thumb" style="width: 120px" alt="" />
+	<el-descriptions :title="title" :column="column" border>
+		<el-descriptions-item v-for="item in list" :span="item.span">
+			<template #label> {{ item.label }} </template>
+			<slot :name="item.prop" :rows="row">
+				{{ item.value || row[item.prop] }}
+			</slot>
 		</el-descriptions-item>
 	</el-descriptions>
 </template>
@@ -35,7 +13,9 @@
 const props = defineProps({
 	data: {
 		type: Object,
-		required: true
+		required: true,
 	}
 });
+const { row, title, column = 2, list } = props.data;
+
 </script>

+ 51 - 63
src/components/table-edit.vue

@@ -1,38 +1,36 @@
 <template>
-	<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
-		<el-form-item label="用户名" prop="name">
-			<el-input v-model="form.name"></el-input>
-		</el-form-item>
-		<el-form-item label="账户余额" prop="money">
-			<el-input v-model.number="form.money"></el-input>
-		</el-form-item>
-		<el-form-item label="地址" prop="address">
-			<el-input v-model="form.address"></el-input>
-		</el-form-item>
-		<el-form-item label="账户状态" prop="state">
-			<el-switch
-				v-model="form.state"
-				:active-value="1"
-				:inactive-value="0"
-				active-text="正常"
-				inactive-text="异常"
-			></el-switch>
-		</el-form-item>
-		<el-form-item label="注册日期" prop="date">
-			<el-date-picker type="date" v-model="form.date" value-format="YYYY-MM-DD"></el-date-picker>
-		</el-form-item>
-		<el-form-item label="上传头像" prop="thumb">
-			<el-upload
-				class="avatar-uploader"
-				action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
-				:show-file-list="false"
-				:on-success="handleAvatarSuccess"
-				:before-upload="beforeAvatarUpload"
-			>
-				<img v-if="form.thumb" :src="form.thumb" class="avatar" />
-				<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
-			</el-upload>
-		</el-form-item>
+	<el-form ref="formRef" :model="form" :rules="rules" :label-width="options.labelWidth">
+		<el-row>
+			<el-col :span="options.span" v-for="item in options.list">
+				<el-form-item :label="item.label" :prop="item.prop">
+					<!-- 文本框、数字框、下拉框、日期框、开关、上传 -->
+					<el-input v-if="item.type === 'input'" v-model="form[item.prop]" :disabled="item.disabled"
+						:placeholder="item.placeholder" clearable></el-input>
+					<el-input-number v-else-if="item.type === 'number'" v-model="form[item.prop]"
+						:disabled="item.disabled" controls-position="right"></el-input-number>
+					<el-select v-else-if="item.type === 'select'" v-model="form[item.prop]" :disabled="item.disabled"
+						:placeholder="item.placeholder" clearable>
+						<el-option v-for="opt in item.opts" :label="opt.label" :value="opt.value"></el-option>
+					</el-select>
+					<el-date-picker v-else-if="item.type === 'date'" type="date" v-model="form[item.prop]"
+						:value-format="item.format"></el-date-picker>
+					<el-switch v-else-if="item.type === 'switch'" v-model="form[item.prop]"
+						:active-value="item.activeValue" :inactive-value="item.inactiveValue"
+						:active-text="item.activeText" :inactive-text="item.inactiveText"></el-switch>
+					<el-upload v-else-if="item.type === 'upload'" class="avatar-uploader" action="#"
+						:show-file-list="false" :on-success="handleAvatarSuccess">
+						<img v-if="form[item.prop]" :src="form[item.prop]" class="avatar" />
+						<el-icon v-else class="avatar-uploader-icon">
+							<Plus />
+						</el-icon>
+					</el-upload>
+					<slot :name="item.prop" v-else>
+
+					</slot>
+				</el-form-item>
+			</el-col>
+		</el-row>
+
 		<el-form-item>
 			<el-button type="primary" @click="saveEdit(formRef)">保 存</el-button>
 		</el-form-item>
@@ -40,11 +38,16 @@
 </template>
 
 <script lang="ts" setup>
-import { ElMessage, FormInstance, FormRules, UploadProps } from 'element-plus';
-import { ref } from 'vue';
+import { FormOption } from '@/types/form-option';
+import { FormInstance, FormRules, UploadProps } from 'element-plus';
+import { PropType, ref } from 'vue';
 
-const props = defineProps({
-	data: {
+const { options, formData, edit, update } = defineProps({
+	options: {
+		type: Object as PropType<FormOption>,
+		required: true
+	},
+	formData: {
 		type: Object,
 		required: true
 	},
@@ -58,28 +61,23 @@ const props = defineProps({
 	}
 });
 
-const defaultData = {
-	id: '',
-	name: '',
-	address: '',
-	thumb: '',
-	money: 0,
-	state: 0,
-	date: new Date()
-};
 
-const form = ref({ ...(props.edit ? props.data : defaultData) });
+const form = ref({ ...(edit ? formData : {}) });
+
+const rules: FormRules = options.list.map(item => {
+	if (item.required) {
+		return { [item.prop]: [{ required: true, message: `${item.label}不能为空`, trigger: 'blur' }] };
+	}
+	return {};
+}).reduce((acc, cur) => ({ ...acc, ...cur }), {});
+
 
-const rules: FormRules = {
-	name: [{ required: true, message: '用户名', trigger: 'blur' }]
-};
 const formRef = ref<FormInstance>();
 const saveEdit = (formEl: FormInstance | undefined) => {
 	if (!formEl) return;
 	formEl.validate(valid => {
 		if (!valid) return false;
-		props.update(form.value);
-		ElMessage.success('保存成功!');
+		update(form.value);
 	});
 };
 
@@ -87,16 +85,6 @@ const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) =>
 	form.value.thumb = URL.createObjectURL(uploadFile.raw!);
 };
 
-const beforeAvatarUpload: UploadProps['beforeUpload'] = rawFile => {
-	if (rawFile.type !== 'image/jpeg') {
-		ElMessage.error('Avatar picture must be JPG format!');
-		return false;
-	} else if (rawFile.size / 1024 / 1024 > 2) {
-		ElMessage.error('Avatar picture size can not exceed 2MB!');
-		return false;
-	}
-	return true;
-};
 </script>
 
 <style>

+ 60 - 0
src/components/table-search.vue

@@ -0,0 +1,60 @@
+<template>
+	<div class="search-container">
+		<el-form ref="searchRef" :model="query" :inline="true">
+			<el-form-item :label="item.label" :prop="item.prop" v-for="item in options">
+				<!-- 文本框、下拉框、日期框 -->
+				<el-input v-if="item.type === 'input'" v-model="query[item.prop]" :disabled="item.disabled"
+					:placeholder="item.placeholder" clearable></el-input>
+				<el-select v-else-if="item.type === 'select'" v-model="query[item.prop]" :disabled="item.disabled"
+					:placeholder="item.placeholder" clearable>
+					<el-option v-for="opt in item.opts" :label="opt.label" :value="opt.value"></el-option>
+				</el-select>
+				<el-date-picker v-else-if="item.type === 'date'" type="date" v-model="query[item.prop]"
+					:value-format="item.format"></el-date-picker>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" :icon="Search" @click="search">搜索</el-button>
+				<el-button :icon="Refresh" @click="resetForm(searchRef)">重置</el-button>
+			</el-form-item>
+		</el-form>
+	</div>
+</template>
+
+<script lang="ts" setup>
+import { FormInstance } from 'element-plus';
+import { Search, Refresh } from '@element-plus/icons-vue';
+import { PropType, ref } from 'vue';
+import { FormOptionList } from '@/types/form-option';
+
+const props = defineProps({
+	query: {
+		type: Object,
+		required: true
+	},
+	options: {
+		type: Array as PropType<Array<FormOptionList>>,
+		required: true
+	},
+	search: {
+		type: Function,
+		default: () => { }
+	}
+});
+
+const searchRef = ref<FormInstance>();
+const resetForm = (formEl: FormInstance | undefined) => {
+	if (!formEl) return
+	formEl.resetFields()
+	props.search();
+}
+</script>
+
+<style scoped>
+.search-container {
+	padding: 20px 30px 0;
+	background-color: #fff;
+	margin-bottom: 10px;
+	border: 1px solid #ddd;
+	border-radius: 5px
+}
+</style>

+ 149 - 0
src/components/tabs.vue

@@ -0,0 +1,149 @@
+<template>
+	<div class="tabs-container">
+		<el-tabs v-model="activePath" class="tabs" type="card" closable @tab-click="clickTabls" @tab-remove="closeTabs">
+			<el-tab-pane v-for="item in tabs.list" :key="item.path" :label="item.title" :name="item.path"
+				@click="setTags(item)"></el-tab-pane>
+		</el-tabs>
+		<div class="Tabs-close-box">
+			<el-dropdown @command="handleTags">
+				<el-button size="small" type="primary" plain>
+					标签选项
+					<el-icon class="el-icon--right">
+						<arrow-down />
+					</el-icon>
+				</el-button>
+				<template #dropdown>
+					<el-dropdown-menu size="small">
+						<el-dropdown-item command="other">关闭其他</el-dropdown-item>
+						<el-dropdown-item command="current">关闭当前</el-dropdown-item>
+						<el-dropdown-item command="all">关闭所有</el-dropdown-item>
+					</el-dropdown-menu>
+				</template>
+			</el-dropdown>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue';
+import { useTabsStore } from '../store/tabs';
+import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
+
+const route = useRoute();
+const router = useRouter();
+const activePath = ref(route.fullPath)
+const tabs = useTabsStore();
+// 设置标签
+const setTags = (route: any) => {
+	const isExist = tabs.list.some(item => {
+		return item.path === route.fullPath;
+	});
+	if (!isExist) {
+		tabs.setTabsItem({
+			name: route.name,
+			title: route.meta.title,
+			path: route.fullPath
+		});
+	}
+};
+setTags(route);
+onBeforeRouteUpdate(to => {
+	setTags(to);
+});
+
+// 关闭全部标签
+const closeAll = () => {
+	tabs.clearTabs();
+	router.push('/');
+};
+// 关闭其他标签
+const closeOther = () => {
+	const curItem = tabs.list.filter(item => {
+		return item.path === route.fullPath;
+	});
+	tabs.closeTabsOther(curItem);
+};
+const handleTags = (command: string) => {
+	switch (command) {
+		case 'current':
+			// 关闭当前页面的标签页
+			tabs.closeCurrentTag({
+				$router: router,
+				$route: route
+			});
+			break;
+		case 'all':
+			closeAll();
+			break;
+
+		case 'other':
+			closeOther();
+			break;
+	}
+};
+
+const clickTabls = (item: any) => {
+	router.push(item.props.name);
+}
+const closeTabs = (path: any) => {
+	console.log(path);
+	const index = tabs.list.findIndex((item) => item.path === path);
+	tabs.delTabsItem(index);
+	const item = tabs.list[index] ? tabs.list[index] : tabs.list[index - 1];
+	if (item) {
+		path === route.fullPath && router.push(item.path);
+	} else {
+		router.push('/');
+	}
+}
+
+watch(() => route.fullPath, (newVal, oldVal) => {
+	activePath.value = newVal;
+})
+
+</script>
+
+<style scss>
+.tabs-container {
+	position: relative;
+	overflow: hidden;
+	background: #fff;
+	padding: 2px 120px 0 0;
+}
+
+.tabs {
+
+	.el-tabs__header {
+		margin-bottom: 0;
+	}
+
+	.el-tabs__nav {
+		height: 28px;
+	}
+
+	.el-tabs__nav-next,
+	.el-tabs__nav-prev {
+		line-height: 32px;
+	}
+
+	&.el-tabs {
+		--el-tabs-header-height: 28px;
+	}
+
+
+}
+
+.Tabs-close-box {
+	position: absolute;
+	right: 0;
+	top: 0;
+	box-sizing: border-box;
+	padding-top: 1px;
+	text-align: center;
+	width: 110px;
+	height: 30px;
+	background: #fff;
+	box-shadow: -3px 0 15px 3px rgba(0, 0, 0, 0.1);
+	z-index: 10;
+}
+</style>

+ 0 - 168
src/components/tags.vue

@@ -1,168 +0,0 @@
-<template>
-	<div class="tags" v-if="tags.show">
-		<ul>
-			<li
-				class="tags-li"
-				v-for="(item, index) in tags.list"
-				:class="{ active: isActive(item.path) }"
-				:key="index"
-			>
-				<router-link :to="item.path" class="tags-li-title">{{ item.title }}</router-link>
-				<el-icon @click="closeTags(index)"><Close /></el-icon>
-			</li>
-		</ul>
-		<div class="tags-close-box">
-			<el-dropdown @command="handleTags">
-				<el-button size="small" type="primary">
-					标签选项
-					<el-icon class="el-icon--right">
-						<arrow-down />
-					</el-icon>
-				</el-button>
-				<template #dropdown>
-					<el-dropdown-menu size="small">
-						<el-dropdown-item command="other">关闭其他</el-dropdown-item>
-						<el-dropdown-item command="all">关闭所有</el-dropdown-item>
-					</el-dropdown-menu>
-				</template>
-			</el-dropdown>
-		</div>
-	</div>
-</template>
-
-<script setup lang="ts">
-import { useTagsStore } from '../store/tags';
-import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
-
-const route = useRoute();
-const router = useRouter();
-const isActive = (path: string) => {
-	return path === route.fullPath;
-};
-
-const tags = useTagsStore();
-// 关闭单个标签
-const closeTags = (index: number) => {
-	const delItem = tags.list[index];
-	tags.delTagsItem(index);
-	const item = tags.list[index] ? tags.list[index] : tags.list[index - 1];
-	if (item) {
-		delItem.path === route.fullPath && router.push(item.path);
-	} else {
-		router.push('/');
-	}
-};
-
-// 设置标签
-const setTags = (route: any) => {
-	const isExist = tags.list.some(item => {
-		return item.path === route.fullPath;
-	});
-	if (!isExist) {
-		if (tags.list.length >= 8) tags.delTagsItem(0);
-		tags.setTagsItem({
-			name: route.name,
-			title: route.meta.title,
-			path: route.fullPath
-		});
-	}
-};
-setTags(route);
-onBeforeRouteUpdate(to => {
-	setTags(to);
-});
-
-// 关闭全部标签
-const closeAll = () => {
-	tags.clearTags();
-	router.push('/');
-};
-// 关闭其他标签
-const closeOther = () => {
-	const curItem = tags.list.filter(item => {
-		return item.path === route.fullPath;
-	});
-	tags.closeTagsOther(curItem);
-};
-const handleTags = (command: string) => {
-	command === 'other' ? closeOther() : closeAll();
-};
-
-// 关闭当前页面的标签页
-// tags.closeCurrentTag({
-//     $router: router,
-//     $route: route
-// });
-</script>
-
-<style>
-.tags {
-	position: relative;
-	height: 30px;
-	overflow: hidden;
-	background: #fff;
-	padding-right: 120px;
-	box-shadow: 0 5px 10px #ddd;
-}
-
-.tags ul {
-	box-sizing: border-box;
-	width: 100%;
-	height: 100%;
-}
-
-.tags-li {
-	display: flex;
-	align-items: center;
-	float: left;
-	margin: 3px 5px 2px 3px;
-	border-radius: 3px;
-	font-size: 12px;
-	overflow: hidden;
-	cursor: pointer;
-	height: 23px;
-	border: 1px solid #e9eaec;
-	background: #fff;
-	padding: 0 5px 0 12px;
-	color: #666;
-	-webkit-transition: all 0.3s ease-in;
-	-moz-transition: all 0.3s ease-in;
-	transition: all 0.3s ease-in;
-}
-
-.tags-li:not(.active):hover {
-	background: #f8f8f8;
-}
-
-.tags-li.active {
-	color: #fff;
-}
-
-.tags-li-title {
-	float: left;
-	max-width: 80px;
-	overflow: hidden;
-	white-space: nowrap;
-	text-overflow: ellipsis;
-	margin-right: 5px;
-	color: #666;
-}
-
-.tags-li.active .tags-li-title {
-	color: #fff;
-}
-
-.tags-close-box {
-	position: absolute;
-	right: 0;
-	top: 0;
-	box-sizing: border-box;
-	padding-top: 1px;
-	text-align: center;
-	width: 110px;
-	height: 30px;
-	background: #fff;
-	box-shadow: -3px 0 15px 3px rgba(0, 0, 0, 0.1);
-	z-index: 10;
-}
-</style>

+ 28 - 28
src/main.ts

@@ -1,28 +1,28 @@
-import { createApp } from 'vue';
-import { createPinia } from 'pinia';
-import * as ElementPlusIconsVue from '@element-plus/icons-vue';
-import App from './App.vue';
-import router from './router';
-import { usePermissStore } from './store/permiss';
-import 'element-plus/dist/index.css';
-import './assets/css/icon.css';
-
-const app = createApp(App);
-app.use(createPinia());
-app.use(router);
-
-// 注册elementplus图标
-for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
-    app.component(key, component);
-}
-// 自定义权限指令
-const permiss = usePermissStore();
-app.directive('permiss', {
-    mounted(el, binding) {
-        if (!permiss.key.includes(String(binding.value))) {
-            el['hidden'] = true;
-        }
-    },
-});
-
-app.mount('#app');
+import { createApp } from 'vue';
+import { createPinia } from 'pinia';
+import * as ElementPlusIconsVue from '@element-plus/icons-vue';
+import App from './App.vue';
+import router from './router';
+import { usePermissStore } from './store/permiss';
+import 'element-plus/dist/index.css';
+import './assets/css/icon.css';
+
+const app = createApp(App);
+app.use(createPinia());
+app.use(router);
+
+// 注册elementplus图标
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+    app.component(key, component);
+}
+// 自定义权限指令
+const permiss = usePermissStore();
+app.directive('permiss', {
+    mounted(el, binding) {
+        if (binding.value && !permiss.key.includes(String(binding.value))) {
+            el['hidden'] = true;
+        }
+    },
+});
+
+app.mount('#app');

+ 293 - 185
src/router/index.ts

@@ -1,185 +1,293 @@
-import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
-import { usePermissStore } from '../store/permiss';
-import Home from '../views/home.vue';
-import NProgress from 'nprogress'
-import 'nprogress/nprogress.css'
-
-const routes: RouteRecordRaw[] = [
-    {
-        path: '/',
-        redirect: '/dashboard',
-    },
-    {
-        path: '/',
-        name: 'Home',
-        component: Home,
-        children: [
-            {
-                path: '/dashboard',
-                name: 'dashboard',
-                meta: {
-                    title: '系统首页',
-                    permiss: '1',
-                },
-                component: () => import(/* webpackChunkName: "dashboard" */ '../views/dashboard.vue'),
-            },
-            {
-                path: '/table',
-                name: 'basetable',
-                meta: {
-                    title: '表格',
-                    permiss: '2',
-                },
-                component: () => import(/* webpackChunkName: "table" */ '../views/table.vue'),
-            },
-            {
-                path: '/charts',
-                name: 'basecharts',
-                meta: {
-                    title: '图表',
-                    permiss: '11',
-                },
-                component: () => import(/* webpackChunkName: "charts" */ '../views/charts.vue'),
-            },
-            {
-                path: '/form',
-                name: 'baseform',
-                meta: {
-                    title: '表单',
-                    permiss: '5',
-                },
-                component: () => import(/* webpackChunkName: "form" */ '../views/form.vue'),
-            },
-            {
-                path: '/tabs',
-                name: 'tabs',
-                meta: {
-                    title: 'tab标签',
-                    permiss: '3',
-                },
-                component: () => import(/* webpackChunkName: "tabs" */ '../views/tabs.vue'),
-            },
-            {
-                path: '/donate',
-                name: 'donate',
-                meta: {
-                    title: '鼓励作者',
-                    permiss: '14',
-                },
-                component: () => import(/* webpackChunkName: "donate" */ '../views/donate.vue'),
-            },
-            {
-                path: '/permission',
-                name: 'permission',
-                meta: {
-                    title: '权限管理',
-                    permiss: '13',
-                },
-                component: () => import(/* webpackChunkName: "permission" */ '../views/permission.vue'),
-            },
-            {
-                path: '/upload',
-                name: 'upload',
-                meta: {
-                    title: '上传插件',
-                    permiss: '6',
-                },
-                component: () => import(/* webpackChunkName: "upload" */ '../views/upload.vue'),
-            },
-            {
-                path: '/icon',
-                name: 'icon',
-                meta: {
-                    title: '自定义图标',
-                    permiss: '10',
-                },
-                component: () => import(/* webpackChunkName: "icon" */ '../views/icon.vue'),
-            },
-            {
-                path: '/user',
-                name: 'user',
-                meta: {
-                    title: '个人中心',
-                },
-                component: () => import(/* webpackChunkName: "user" */ '../views/user.vue'),
-            },
-            {
-                path: '/editor',
-                name: 'editor',
-                meta: {
-                    title: '富文本编辑器',
-                    permiss: '8',
-                },
-                component: () => import(/* webpackChunkName: "editor" */ '../views/editor.vue'),
-            },
-            {
-                path: '/markdown',
-                name: 'markdown',
-                meta: {
-                    title: 'markdown编辑器',
-                    permiss: '9',
-                },
-                component: () => import(/* webpackChunkName: "markdown" */ '../views/markdown.vue'),
-            },
-            {
-                path: '/export',
-                name: 'export',
-                meta: {
-                    title: '导出Excel',
-                    permiss: '2',
-                },
-                component: () => import(/* webpackChunkName: "export" */ '../views/export.vue'),
-            },
-            {
-                path: '/import',
-                name: 'import',
-                meta: {
-                    title: '导入Excel',
-                    permiss: '2',
-                },
-                component: () => import(/* webpackChunkName: "import" */ '../views/import.vue'),
-            },
-        ],
-    },
-    {
-        path: '/login',
-        name: 'Login',
-        meta: {
-            title: '登录',
-        },
-        component: () => import(/* webpackChunkName: "login" */ '../views/login.vue'),
-    },
-    {
-        path: '/403',
-        name: '403',
-        meta: {
-            title: '没有权限',
-        },
-        component: () => import(/* webpackChunkName: "403" */ '../views/403.vue'),
-    },
-];
-
-const router = createRouter({
-    history: createWebHashHistory(),
-    routes,
-});
-
-router.beforeEach((to, from, next) => {
-    NProgress.start();
-    const role = localStorage.getItem('ms_username');
-    const permiss = usePermissStore();
-    if (!role && to.path !== '/login') {
-        next('/login');
-    } else if (to.meta.permiss && !permiss.key.includes(to.meta.permiss)) {
-        // 如果没有权限,则进入403
-        next('/403');
-    } else {
-        next();
-    }
-});
-
-router.afterEach(() => {
-    NProgress.done()
-})
-
-export default router;
+import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
+import { usePermissStore } from '../store/permiss';
+import Home from '../views/home.vue';
+import NProgress from 'nprogress';
+import 'nprogress/nprogress.css';
+
+const routes: RouteRecordRaw[] = [
+    {
+        path: '/',
+        redirect: '/dashboard',
+    },
+    {
+        path: '/',
+        name: 'Home',
+        component: Home,
+        children: [
+            {
+                path: '/dashboard',
+                name: 'dashboard',
+                meta: {
+                    title: '系统首页',
+                    permiss: '0',
+                },
+                component: () => import(/* webpackChunkName: "dashboard" */ '../views/dashboard.vue'),
+            },
+            {
+                path: '/system-user',
+                name: 'system-user',
+                meta: {
+                    title: '用户管理',
+                    permiss: '11',
+                },
+                component: () => import(/* webpackChunkName: "system-user" */ '../views/system/user.vue'),
+            },
+            {
+                path: '/system-role',
+                name: 'system-role',
+                meta: {
+                    title: '角色管理',
+                    permiss: '12',
+                },
+                component: () => import(/* webpackChunkName: "system-role" */ '../views/system/role.vue'),
+            },
+            {
+                path: '/system-menu',
+                name: 'system-menu',
+                meta: {
+                    title: '菜单管理',
+                    permiss: '13',
+                },
+                component: () => import(/* webpackChunkName: "system-menu" */ '../views/system/menu.vue'),
+            },
+            {
+                path: '/table',
+                name: 'basetable',
+                meta: {
+                    title: '基础表格',
+                    permiss: '31',
+                },
+                component: () => import(/* webpackChunkName: "table" */ '../views/table/basetable.vue'),
+            },
+            {
+                path: '/table-editor',
+                name: 'table-editor',
+                meta: {
+                    title: '可编辑表格',
+                    permiss: '32',
+                },
+                component: () => import(/* webpackChunkName: "table-editor" */ '../views/table/table-editor.vue'),
+            },
+            {
+                path: '/schart',
+                name: 'schart',
+                meta: {
+                    title: 'schart图表',
+                    permiss: '41',
+                },
+                component: () => import(/* webpackChunkName: "schart" */ '../views/chart/schart.vue'),
+            },
+            {
+                path: '/echarts',
+                name: 'echarts',
+                meta: {
+                    title: 'echarts图表',
+                    permiss: '42',
+                },
+                component: () => import(/* webpackChunkName: "echarts" */ '../views/chart/echarts.vue'),
+            },
+
+            {
+                path: '/icon',
+                name: 'icon',
+                meta: {
+                    title: '图标',
+                    permiss: '5',
+                },
+                component: () => import(/* webpackChunkName: "icon" */ '../views/pages/icon.vue'),
+            },
+            {
+                path: '/ucenter',
+                name: 'ucenter',
+                meta: {
+                    title: '个人中心',
+                },
+                component: () => import(/* webpackChunkName: "ucenter" */ '../views/pages/ucenter.vue'),
+            },
+            {
+                path: '/editor',
+                name: 'editor',
+                meta: {
+                    title: '富文本编辑器',
+                    permiss: '291',
+                },
+                component: () => import(/* webpackChunkName: "editor" */ '../views/pages/editor.vue'),
+            },
+            {
+                path: '/markdown',
+                name: 'markdown',
+                meta: {
+                    title: 'markdown编辑器',
+                    permiss: '292',
+                },
+                component: () => import(/* webpackChunkName: "markdown" */ '../views/pages/markdown.vue'),
+            },
+            {
+                path: '/export',
+                name: 'export',
+                meta: {
+                    title: '导出Excel',
+                    permiss: '34',
+                },
+                component: () => import(/* webpackChunkName: "export" */ '../views/table/export.vue'),
+            },
+            {
+                path: '/import',
+                name: 'import',
+                meta: {
+                    title: '导入Excel',
+                    permiss: '33',
+                },
+                component: () => import(/* webpackChunkName: "import" */ '../views/table/import.vue'),
+            },
+            {
+                path: '/theme',
+                name: 'theme',
+                meta: {
+                    title: '主题设置',
+                    permiss: '7',
+                },
+                component: () => import(/* webpackChunkName: "theme" */ '../views/pages/theme.vue'),
+            },
+            {
+                path: '/calendar',
+                name: 'calendar',
+                meta: {
+                    title: '日历',
+                    permiss: '24',
+                },
+                component: () => import(/* webpackChunkName: "calendar" */ '../views/element/calendar.vue'),
+            },
+            {
+                path: '/watermark',
+                name: 'watermark',
+                meta: {
+                    title: '水印',
+                    permiss: '25',
+                },
+                component: () => import(/* webpackChunkName: "watermark" */ '../views/element/watermark.vue'),
+            },
+            {
+                path: '/carousel',
+                name: 'carousel',
+                meta: {
+                    title: '走马灯',
+                    permiss: '23',
+                },
+                component: () => import(/* webpackChunkName: "carousel" */ '../views/element/carousel.vue'),
+            },
+            {
+                path: '/tour',
+                name: 'tour',
+                meta: {
+                    title: '分步引导',
+                    permiss: '26',
+                },
+                component: () => import(/* webpackChunkName: "tour" */ '../views/element/tour.vue'),
+            },
+            {
+                path: '/steps',
+                name: 'steps',
+                meta: {
+                    title: '步骤条',
+                    permiss: '27',
+                },
+                component: () => import(/* webpackChunkName: "steps" */ '../views/element/steps.vue'),
+            },
+            {
+                path: '/form',
+                name: 'forms',
+                meta: {
+                    title: '表单',
+                    permiss: '21',
+                },
+                component: () => import(/* webpackChunkName: "form" */ '../views/element/form.vue'),
+            },
+            {
+                path: '/upload',
+                name: 'upload',
+                meta: {
+                    title: '上传',
+                    permiss: '22',
+                },
+                component: () => import(/* webpackChunkName: "upload" */ '../views/element/upload.vue'),
+            },
+            {
+                path: '/statistic',
+                name: 'statistic',
+                meta: {
+                    title: '统计',
+                    permiss: '28',
+                },
+                component: () => import(/* webpackChunkName: "statistic" */ '../views/element/statistic.vue'),
+            },
+        ],
+    },
+    {
+        path: '/login',
+        meta: {
+            title: '登录',
+            noAuth: true,
+        },
+        component: () => import(/* webpackChunkName: "login" */ '../views/pages/login.vue'),
+    },
+    {
+        path: '/register',
+        meta: {
+            title: '注册',
+            noAuth: true,
+        },
+        component: () => import(/* webpackChunkName: "register" */ '../views/pages/register.vue'),
+    },
+    {
+        path: '/reset-pwd',
+        meta: {
+            title: '重置密码',
+            noAuth: true,
+        },
+        component: () => import(/* webpackChunkName: "reset-pwd" */ '../views/pages/reset-pwd.vue'),
+    },
+    {
+        path: '/403',
+        meta: {
+            title: '没有权限',
+            noAuth: true,
+        },
+        component: () => import(/* webpackChunkName: "403" */ '../views/pages/403.vue'),
+    },
+    {
+        path: '/404',
+        meta: {
+            title: '找不到页面',
+            noAuth: true,
+        },
+        component: () => import(/* webpackChunkName: "404" */ '../views/pages/404.vue'),
+    },
+    { path: '/:path(.*)', redirect: '/404' },
+];
+
+const router = createRouter({
+    history: createWebHashHistory(),
+    routes,
+});
+
+router.beforeEach((to, from, next) => {
+    NProgress.start();
+    const role = localStorage.getItem('ms_username');
+    const permiss = usePermissStore();
+
+    if (!role && to.meta.noAuth !== true) {
+        next('/login');
+    } else if (to.meta.permiss && !permiss.key.includes(to.meta.permiss)) {
+        // 如果没有权限,则进入403
+        next('/403');
+    } else {
+        next();
+    }
+});
+
+router.afterEach(() => {
+    NProgress.done();
+});
+
+export default router;

+ 58 - 23
src/store/permiss.ts

@@ -1,23 +1,58 @@
-import { defineStore } from 'pinia';
-
-interface ObjectList {
-	[key: string]: string[];
-}
-
-export const usePermissStore = defineStore('permiss', {
-	state: () => {
-		const keys = localStorage.getItem('ms_keys');
-		return {
-			key: keys ? JSON.parse(keys) : <string[]>[],
-			defaultList: <ObjectList>{
-				admin: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16'],
-				user: ['1', '2', '3', '11', '13', '14', '15']
-			}
-		};
-	},
-	actions: {
-		handleSet(val: string[]) {
-			this.key = val;
-		}
-	}
-});
+import { defineStore } from 'pinia';
+
+interface ObjectList {
+    [key: string]: string[];
+}
+
+export const usePermissStore = defineStore('permiss', {
+    state: () => {
+        const keys = localStorage.getItem('ms_keys');
+        return {
+            key: keys ? JSON.parse(keys) : <string[]>[],
+            defaultList: <ObjectList>{
+                admin: [
+                    '0',
+                    '1',
+                    '11',
+                    '12',
+                    '13',
+                    '2',
+                    '21',
+                    '22',
+                    '23',
+                    '24',
+                    '25',
+                    '26',
+                    '27',
+                    '28',
+                    '29',
+                    '291',
+                    '292',
+                    '3',
+                    '31',
+                    '32',
+                    '33',
+                    '34',
+                    '4',
+                    '41',
+                    '42',
+                    '5',
+                    '7',
+                    '6',
+                    '61',
+                    '62',
+                    '63',
+                    '64',
+                    '65',
+                    '66',
+                ],
+                user: ['0', '1', '11', '12', '13'],
+            },
+        };
+    },
+    actions: {
+        handleSet(val: string[]) {
+            this.key = val;
+        },
+    },
+});

+ 25 - 15
src/store/sidebar.ts

@@ -1,15 +1,25 @@
-import { defineStore } from 'pinia';
-
-export const useSidebarStore = defineStore('sidebar', {
-	state: () => {
-		return {
-			collapse: false
-		};
-	},
-	getters: {},
-	actions: {
-		handleCollapse() {
-			this.collapse = !this.collapse;
-		}
-	}
-});
+import { defineStore } from 'pinia';
+
+export const useSidebarStore = defineStore('sidebar', {
+	state: () => {
+		return {
+			collapse: false,
+			bgColor: localStorage.getItem('sidebar-bg-color') || '#324157',
+			textColor: localStorage.getItem('sidebar-text-color') || '#bfcbd9'
+		};
+	},
+	getters: {},
+	actions: {
+		handleCollapse() {
+			this.collapse = !this.collapse;
+		},
+		setBgColor(color: string) {
+			this.bgColor = color;
+			localStorage.setItem('sidebar-bg-color', color);
+		},
+		setTextColor(color: string) {
+			this.textColor = color;
+			localStorage.setItem('sidebar-text-color', color);
+		}
+	}
+});

+ 53 - 53
src/store/tags.ts → src/store/tabs.ts

@@ -1,53 +1,53 @@
-import { defineStore } from 'pinia';
-
-interface ListItem {
-	name: string;
-	path: string;
-	title: string;
-}
-
-export const useTagsStore = defineStore('tags', {
-	state: () => {
-		return {
-			list: <ListItem[]>[]
-		};
-	},
-	getters: {
-		show: state => {
-			return state.list.length > 0;
-		},
-		nameList: state => {
-			return state.list.map(item => item.name);
-		}
-	},
-	actions: {
-		delTagsItem(index: number) {
-			this.list.splice(index, 1);
-		},
-		setTagsItem(data: ListItem) {
-			this.list.push(data);
-		},
-		clearTags() {
-			this.list = [];
-		},
-		closeTagsOther(data: ListItem[]) {
-			this.list = data;
-		},
-		closeCurrentTag(data: any) {
-			for (let i = 0, len = this.list.length; i < len; i++) {
-				const item = this.list[i];
-				if (item.path === data.$route.fullPath) {
-					if (i < len - 1) {
-						data.$router.push(this.list[i + 1].path);
-					} else if (i > 0) {
-						data.$router.push(this.list[i - 1].path);
-					} else {
-						data.$router.push('/');
-					}
-					this.list.splice(i, 1);
-					break;
-				}
-			}
-		}
-	}
-});
+import { defineStore } from 'pinia';
+
+interface ListItem {
+	name: string;
+	path: string;
+	title: string;
+}
+
+export const useTabsStore = defineStore('tabs', {
+	state: () => {
+		return {
+			list: <ListItem[]>[]
+		};
+	},
+	getters: {
+		show: state => {
+			return state.list.length > 0;
+		},
+		nameList: state => {
+			return state.list.map(item => item.name);
+		}
+	},
+	actions: {
+		delTabsItem(index: number) {
+			this.list.splice(index, 1);
+		},
+		setTabsItem(data: ListItem) {
+			this.list.push(data);
+		},
+		clearTabs() {
+			this.list = [];
+		},
+		closeTabsOther(data: ListItem[]) {
+			this.list = data;
+		},
+		closeCurrentTag(data: any) {
+			for (let i = 0, len = this.list.length; i < len; i++) {
+				const item = this.list[i];
+				if (item.path === data.$route.fullPath) {
+					if (i < len - 1) {
+						data.$router.push(this.list[i + 1].path);
+					} else if (i > 0) {
+						data.$router.push(this.list[i - 1].path);
+					} else {
+						data.$router.push('/');
+					}
+					this.list.splice(i, 1);
+					break;
+				}
+			}
+		}
+	}
+});

+ 58 - 0
src/store/theme.ts

@@ -0,0 +1,58 @@
+import { mix, setProperty } from '@/utils';
+import { defineStore } from 'pinia';
+
+export const useThemeStore = defineStore('theme', {
+    state: () => {
+        return {
+            primary: '',
+            success: '',
+            warning: '',
+            danger: '',
+            info: '',
+            headerBgColor: '#242f42',
+            headerTextColor: '#fff',
+        };
+    },
+    getters: {},
+    actions: {
+        initTheme() {
+            ['primary', 'success', 'warning', 'danger', 'info'].forEach((type) => {
+                const color = localStorage.getItem(`theme-${type}`) || '';
+                if (color) {
+                    this.setPropertyColor(color, type); // 设置主题色
+                }
+            });
+            const headerBgColor = localStorage.getItem('header-bg-color');
+            headerBgColor && this.setHeaderBgColor(headerBgColor);
+            const headerTextColor = localStorage.getItem('header-text-color');
+            headerTextColor && this.setHeaderTextColor(headerTextColor);
+        },
+        resetTheme() {
+            ['primary', 'success', 'warning', 'danger', 'info'].forEach((type) => {
+                this.setPropertyColor('', type); // 重置主题色
+            });
+        },
+        setPropertyColor(color: string, type: string = 'primary') {
+            this[type] = color;
+            setProperty(`--el-color-${type}`, color);
+            localStorage.setItem(`theme-${type}`, color);
+            this.setThemeLight(type);
+        },
+        setThemeLight(type: string = 'primary') {
+            [3, 5, 7, 8, 9].forEach((v) => {
+                setProperty(`--el-color-${type}-light-${v}`, mix('#ffffff', this[type], v / 10));
+            });
+            setProperty(`--el-color-${type}-dark-2`, mix('#ffffff', this[type], 0.2));
+        },
+        setHeaderBgColor(color: string) {
+            this.headerBgColor = color;
+            setProperty('--header-bg-color', color);
+            localStorage.setItem(`header-bg-color`, color);
+        },
+        setHeaderTextColor(color: string) {
+            this.headerTextColor = color;
+            setProperty('--header-text-color', color);
+            localStorage.setItem(`header-text-color`, color);
+        }
+    }
+});

+ 21 - 0
src/types/form-option.ts

@@ -0,0 +1,21 @@
+export interface FormOption {
+    list: FormOptionList[];
+    labelWidth?: number | string;
+    span?: number;
+
+}
+
+export interface FormOptionList {
+    prop: string;
+    label: string;
+    type: string;
+    placeholder?: string;
+    disabled?: boolean;
+    opts?: any[];
+    format?: string;
+    activeValue?: any;
+    inactiveValue?: any;
+    activeText?: string;
+    inactiveText?: string;
+    required?: boolean;
+}

+ 9 - 0
src/types/menu.ts

@@ -0,0 +1,9 @@
+export interface Menus {
+    id: string;
+    pid?: string;
+    icon?: string;
+    index: string;
+    title: string;
+    permiss?: string;
+    children?: Menus[];
+}

+ 8 - 0
src/types/role.ts

@@ -0,0 +1,8 @@
+
+export interface Role {
+    id: number;
+    name: string;
+    key: string;
+    status: boolean;
+    permiss: string[]
+}

+ 9 - 0
src/types/table.ts

@@ -0,0 +1,9 @@
+export interface TableItem {
+    id: number;
+    name: string;
+    thumb: string;
+    money: number;
+    state: string;
+    date: string;
+    address: string;
+}

+ 16 - 0
src/types/user.ts

@@ -0,0 +1,16 @@
+
+export interface User {
+    id: number;
+    name: string;
+    password: string;
+    email: string;
+    phone: string;
+    role: string;
+    date: string;
+}
+
+export interface Register {
+    username: string;
+    password: string;
+    email: string;
+}

File diff suppressed because it is too large
+ 0 - 0
src/utils/china.ts


+ 14 - 0
src/utils/index.ts

@@ -0,0 +1,14 @@
+export const setProperty = (prop: string, val: any, dom = document.documentElement) => {
+    dom.style.setProperty(prop, val);
+};
+
+export const mix = (color1: string, color2: string, weight: number = 0.5): string => {
+    let color = '#';
+    for (let i = 0; i <= 2; i++) {
+        const c1 = parseInt(color1.substring(1 + i * 2, 3 + i * 2), 16);
+        const c2 = parseInt(color2.substring(1 + i * 2, 3 + i * 2), 16);
+        const c = Math.round(c1 * weight + c2 * (1 - weight));
+        color += c.toString(16).padStart(2, '0');
+    }
+    return color;
+};

+ 31 - 31
src/utils/request.ts

@@ -1,31 +1,31 @@
-import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
-
-const service: AxiosInstance = axios.create({
-    timeout: 5000
-});
-
-service.interceptors.request.use(
-    (config: InternalAxiosRequestConfig) => {
-        return config;
-    },
-    (error: AxiosError) => {
-        console.log(error);
-        return Promise.reject();
-    }
-);
-
-service.interceptors.response.use(
-    (response: AxiosResponse) => {
-        if (response.status === 200) {
-            return response;
-        } else {
-            Promise.reject();
-        }
-    },
-    (error: AxiosError) => {
-        console.log(error);
-        return Promise.reject();
-    }
-);
-
-export default service;
+import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
+
+const service: AxiosInstance = axios.create({
+    timeout: 5000
+});
+
+service.interceptors.request.use(
+    (config: InternalAxiosRequestConfig) => {
+        return config;
+    },
+    (error: AxiosError) => {
+        console.log(error);
+        return Promise.reject();
+    }
+);
+
+service.interceptors.response.use(
+    (response: AxiosResponse) => {
+        if (response.status === 200) {
+            return response;
+        } else {
+            Promise.reject();
+        }
+    },
+    (error: AxiosError) => {
+        console.log(error);
+        return Promise.reject();
+    }
+);
+
+export default service;

+ 0 - 54
src/views/403.vue

@@ -1,54 +0,0 @@
-<template>
-	<div class="error-page">
-		<div class="error-code">4<span>0</span>3</div>
-		<div class="error-desc">啊哦~ 你没有权限访问该页面哦</div>
-		<div class="error-handle">
-			<router-link to="/">
-				<el-button type="primary" size="large">返回首页</el-button>
-			</router-link>
-			<el-button class="error-btn" type="primary" size="large" @click="goBack">返回上一页</el-button>
-		</div>
-	</div>
-</template>
-
-<script setup lang="ts" name="403">
-import { useRouter } from 'vue-router';
-
-const router = useRouter();
-const goBack = () => {
-	router.go(-2);
-};
-</script>
-
-<style scoped>
-.error-page {
-	display: flex;
-	justify-content: center;
-	align-items: center;
-	flex-direction: column;
-	width: 100%;
-	height: 100%;
-	background: #f3f3f3;
-	box-sizing: border-box;
-}
-.error-code {
-	line-height: 1;
-	font-size: 250px;
-	font-weight: bolder;
-	color: #f02d2d;
-}
-.error-code span {
-	color: #00a854;
-}
-.error-desc {
-	font-size: 30px;
-	color: #777;
-}
-.error-handle {
-	margin-top: 30px;
-	padding-bottom: 200px;
-}
-.error-btn {
-	margin-left: 100px;
-}
-</style>

+ 0 - 54
src/views/404.vue

@@ -1,54 +0,0 @@
-<template>
-	<div class="error-page">
-		<div class="error-code">4<span>0</span>4</div>
-		<div class="error-desc">啊哦~ 你所访问的页面不存在</div>
-		<div class="error-handle">
-			<router-link to="/">
-				<el-button type="primary" size="large">返回首页</el-button>
-			</router-link>
-			<el-button class="error-btn" type="primary" size="large" @click="goBack">返回上一页</el-button>
-		</div>
-	</div>
-</template>
-
-<script setup lang="ts" name="404">
-import { useRouter } from 'vue-router';
-
-const router = useRouter();
-const goBack = () => {
-	router.go(-1);
-};
-</script>
-
-<style scoped>
-.error-page {
-	display: flex;
-	justify-content: center;
-	align-items: center;
-	flex-direction: column;
-	width: 100%;
-	height: 100%;
-	background: #f3f3f3;
-	box-sizing: border-box;
-}
-.error-code {
-	line-height: 1;
-	font-size: 250px;
-	font-weight: bolder;
-	color: #2d8cf0;
-}
-.error-code span {
-	color: #00a854;
-}
-.error-desc {
-	font-size: 30px;
-	color: #777;
-}
-.error-handle {
-	margin-top: 30px;
-	padding-bottom: 200px;
-}
-.error-btn {
-	margin-left: 100px;
-}
-</style>

+ 87 - 0
src/views/chart/echarts.vue

@@ -0,0 +1,87 @@
+<template>
+    <div class="container">
+        <div class="plugins-tips">
+            vue-echarts:Apache ECharts™ 的 Vue.js 组件。 访问地址:
+            <a href="https://github.com/ecomfe/vue-echarts" target="_blank">vue-echarts</a>
+        </div>
+        <el-card class="mgb20" shadow="hover">
+            <template #header>
+                <div class="content-title">柱状图</div>
+            </template>
+            <v-chart class="schart" :option="barOptions" />
+        </el-card>
+        <el-card class="mgb20" shadow="hover">
+            <template #header>
+                <div class="content-title">折线图</div>
+            </template>
+            <v-chart class="schart" :option="lineOptions" />
+        </el-card>
+        <el-card class="mgb20" shadow="hover">
+            <template #header>
+                <div class="content-title">饼状图</div>
+            </template>
+            <v-chart class="schart" :option="pieOptions" />
+        </el-card>
+        <el-card class="mgb20" shadow="hover">
+            <template #header>
+                <div class="content-title">环形图</div>
+            </template>
+            <v-chart class="schart" :option="ringOptions" />
+        </el-card>
+        <el-card class="mgb20" shadow="hover">
+            <template #header>
+                <div class="content-title">词云图</div>
+            </template>
+            <v-chart class="schart" :option="wordOptions" />
+        </el-card>
+        <el-card class="mgb20" shadow="hover">
+            <template #header>
+                <div class="content-title">地图</div>
+            </template>
+            <v-chart class="schart" :option="mapOptions" />
+        </el-card>
+    </div>
+</template>
+
+<script setup lang="ts" name="echarts">
+import { registerMap, use } from 'echarts/core';
+import { BarChart, LineChart, PieChart, MapChart } from 'echarts/charts';
+import {
+    GridComponent,
+    TooltipComponent,
+    LegendComponent,
+    TitleComponent,
+    VisualMapComponent,
+} from 'echarts/components';
+import { CanvasRenderer } from 'echarts/renderers';
+import VChart from 'vue-echarts';
+import 'echarts-wordcloud';
+import { barOptions, lineOptions, pieOptions, ringOptions, wordOptions, mapOptions } from './options';
+import chinaMap from '@/utils/china';
+use([
+    CanvasRenderer,
+    BarChart,
+    GridComponent,
+    LineChart,
+    PieChart,
+    MapChart,
+    TooltipComponent,
+    LegendComponent,
+    TitleComponent,
+    VisualMapComponent,
+]);
+registerMap('china', chinaMap);
+</script>
+
+<style scoped>
+.schart {
+    width: 100%;
+    height: 400px;
+}
+
+.content-title {
+    font-weight: 400;
+    font-size: 22px;
+    color: #1f2f3d;
+}
+</style>

+ 345 - 0
src/views/chart/options.ts

@@ -0,0 +1,345 @@
+import { graphic } from 'echarts/core';
+export const barOptions = {
+    xAxis: {
+        type: 'category',
+        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+    },
+    yAxis: {
+        type: 'value',
+    },
+    tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+            type: 'shadow',
+        },
+    },
+    color: ['#009688', '#f44336'],
+    series: [
+        {
+            data: [120, 200, 150, 80, 70, 110, 130],
+            type: 'bar',
+        },
+        {
+            data: [180, 230, 190, 120, 110, 230, 235],
+            type: 'bar',
+        },
+    ],
+};
+
+export const lineOptions = {
+    tooltip: {
+        trigger: 'axis',
+    },
+    grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '3%',
+        containLabel: true,
+    },
+    xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+    },
+    yAxis: {
+        type: 'value',
+    },
+    color: ['#009688', '#f44336'],
+    series: [
+        {
+            name: 'Email',
+            type: 'line',
+            stack: 'Total',
+            areaStyle: {},
+            smooth: true,
+            data: [120, 132, 101, 134, 90, 230, 210],
+        },
+        {
+            name: 'Union Ads',
+            type: 'line',
+            stack: 'Total',
+            areaStyle: {},
+            smooth: true,
+            data: [220, 182, 191, 234, 290, 330, 310],
+        },
+    ],
+};
+
+export const pieOptions = {
+    title: {
+        text: 'Referer of a Website',
+        subtext: 'Fake Data',
+        left: 'center',
+    },
+    tooltip: {
+        trigger: 'item',
+    },
+    legend: {
+        orient: 'vertical',
+        left: 'left',
+    },
+    series: [
+        {
+            name: 'Access From',
+            type: 'pie',
+            radius: '50%',
+            data: [
+                { value: 1048, name: 'Search Engine' },
+                { value: 735, name: 'Direct' },
+                { value: 580, name: 'Email' },
+                { value: 484, name: 'Union Ads' },
+                { value: 300, name: 'Video Ads' },
+            ],
+            emphasis: {
+                itemStyle: {
+                    shadowBlur: 10,
+                    shadowOffsetX: 0,
+                    shadowColor: 'rgba(0, 0, 0, 0.5)',
+                },
+            },
+        },
+    ],
+};
+
+export const wordOptions = {
+    series: [
+        {
+            type: 'wordCloud',
+            rotationRange: [0, 0],
+            autoSize: {
+                enable: true,
+                minSize: 14,
+            },
+            textStyle: {
+                fontFamily: '微软雅黑,sans-serif',
+                color: function () {
+                    return (
+                        'rgb(' +
+                        [
+                            Math.round(Math.random() * 160),
+                            Math.round(Math.random() * 160),
+                            Math.round(Math.random() * 160),
+                        ].join(',') +
+                        ')'
+                    );
+                },
+            },
+            data: [
+                {
+                    name: 'Vue',
+                    value: 10000,
+                },
+                {
+                    name: 'React',
+                    value: 9000,
+                },
+                {
+                    name: '图表',
+                    value: 4000,
+                },
+                {
+                    name: '产品',
+                    value: 7000,
+                },
+                {
+                    name: 'vue-manage-system',
+                    value: 2000,
+                },
+                {
+                    name: 'element-plus',
+                    value: 6000,
+                },
+                {
+                    name: '管理系统',
+                    value: 5000,
+                },
+                {
+                    name: '前端',
+                    value: 4000,
+                },
+                {
+                    name: '测试',
+                    value: 3000,
+                },
+                {
+                    name: '后端',
+                    value: 8000,
+                },
+                {
+                    name: '软件开发',
+                    value: 6000,
+                },
+                {
+                    name: '程序员',
+                    value: 4000,
+                },
+            ],
+        },
+    ],
+};
+
+export const ringOptions = {
+    tooltip: {
+        trigger: 'item',
+    },
+    legend: {
+        top: '5%',
+        left: 'center',
+    },
+
+    series: [
+        {
+            name: 'Access From',
+            type: 'pie',
+            radius: ['40%', '70%'],
+            avoidLabelOverlap: false,
+            itemStyle: {
+                borderRadius: 10,
+                borderColor: '#fff',
+                borderWidth: 2,
+            },
+            label: {
+                show: false,
+                position: 'center',
+            },
+            emphasis: {
+                label: {
+                    show: true,
+                    fontSize: 40,
+                    fontWeight: 'bold',
+                },
+            },
+            labelLine: {
+                show: false,
+            },
+            data: [
+                { value: 1048, name: 'Search Engine' },
+                { value: 735, name: 'Direct' },
+                { value: 580, name: 'Email' },
+                { value: 484, name: 'Union Ads' },
+                { value: 300, name: 'Video Ads' },
+            ],
+        },
+    ],
+};
+
+export const dashOpt1 = {
+    xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+    },
+    yAxis: {
+        type: 'value',
+    },
+    grid: {
+        top: '2%',
+        left: '2%',
+        right: '3%',
+        bottom: '2%',
+        containLabel: true,
+    },
+    color: ['#009688', '#f44336'],
+    series: [
+        {
+            type: 'line',
+            areaStyle: {
+                color: new graphic.LinearGradient(0, 0, 0, 1, [
+                    {
+                        offset: 0,
+                        color: 'rgba(0, 150, 136,0.8)',
+                    },
+                    {
+                        offset: 1,
+                        color: 'rgba(0, 150, 136,0.2)',
+                    },
+                ]),
+            },
+            smooth: true,
+            data: [120, 132, 301, 134, 90, 230, 210],
+        },
+        {
+            type: 'line',
+            smooth: true,
+            data: [220, 122, 191, 234, 190, 130, 310],
+        },
+    ],
+};
+
+export const dashOpt2 = {
+    legend: {
+        bottom: '1%',
+        left: 'center',
+    },
+    color: ['#3f51b5', '#009688', '#f44336', '#00bcd4', '#1ABC9C'],
+    series: [
+        {
+            type: 'pie',
+            radius: ['40%', '70%'],
+            avoidLabelOverlap: false,
+            itemStyle: {
+                borderRadius: 10,
+                borderColor: '#fff',
+                borderWidth: 2,
+            },
+            data: [
+                { value: 1048, name: '数码' },
+                { value: 735, name: '食品' },
+                { value: 580, name: '母婴' },
+                { value: 484, name: '家电' },
+                { value: 300, name: '运动' },
+            ],
+        },
+    ],
+};
+
+export const mapOptions = {
+    tooltip: {
+        trigger: 'item',
+    },
+    geo: {
+        map: 'china',
+        roam: false,
+        emphasis: {
+            label: {
+                show: false,
+            },
+        },
+    },
+    visualMap: {
+        show: false,
+        min: 0,
+        max: 100,
+        realtime: false,
+        calculable: false,
+        inRange: {
+            color: ['#d2e0f5', '#71A9FF'],
+        },
+    },
+    series: [
+        {
+            geoIndex: 0,
+            name: '地域分布',
+            type: 'map',
+            coordinateSystem: 'geo',
+            map: 'china',
+            data: [
+                { name: '北京', value: 100 },
+                { name: '上海', value: 100 },
+                { name: '广东', value: 100 },
+                { name: '浙江', value: 90 },
+                { name: '江西', value: 80 },
+                { name: '山东', value: 70 },
+                { name: '广西', value: 60 },
+                { name: '河南', value: 50 },
+                { name: '河南', value: 40 },
+                { name: '青海', value: 70 },
+                { name: '河南', value: 30 },
+                { name: '黑龙江', value: 20 },
+                { name: '新疆', value: 20 },
+                { name: '云南', value: 20 },
+                { name: '甘肃', value: 20 },
+            ],
+        },
+    ],
+};

+ 129 - 127
src/views/charts.vue → src/views/chart/schart.vue

@@ -1,127 +1,129 @@
-<template>
-	<div class="container">
-		<div class="plugins-tips">
-			vue-schart:vue.js封装sChart.js的图表组件。 访问地址:
-			<a href="https://github.com/lin-xin/vue-schart" target="_blank">vue-schart</a>
-		</div>
-		<div class="schart-box">
-			<div class="content-title">柱状图</div>
-			<schart class="schart" canvasId="bar" :options="options1"></schart>
-		</div>
-		<div class="schart-box">
-			<div class="content-title">折线图</div>
-			<schart class="schart" canvasId="line" :options="options2"></schart>
-		</div>
-		<div class="schart-box">
-			<div class="content-title">饼状图</div>
-			<schart class="schart" canvasId="pie" :options="options3"></schart>
-		</div>
-		<div class="schart-box">
-			<div class="content-title">环形图</div>
-			<schart class="schart" canvasId="ring" :options="options4"></schart>
-		</div>
-	</div>
-</template>
-
-<script setup lang="ts" name="basecharts">
-import Schart from 'vue-schart';
-
-const options1 = {
-	type: 'bar',
-	title: {
-		text: '最近一周各品类销售图'
-	},
-	bgColor: '#fbfbfb',
-	labels: ['周一', '周二', '周三', '周四', '周五'],
-	datasets: [
-		{
-			label: '家电',
-			fillColor: 'rgba(241, 49, 74, 0.5)',
-			data: [234, 278, 270, 190, 230]
-		},
-		{
-			label: '百货',
-			data: [164, 178, 190, 135, 160]
-		},
-		{
-			label: '食品',
-			data: [144, 198, 150, 235, 120]
-		}
-	]
-};
-const options2 = {
-	type: 'line',
-	title: {
-		text: '最近几个月各品类销售趋势图'
-	},
-	bgColor: '#fbfbfb',
-	labels: ['6月', '7月', '8月', '9月', '10月'],
-	datasets: [
-		{
-			label: '家电',
-			data: [234, 278, 270, 190, 230]
-		},
-		{
-			label: '百货',
-			data: [164, 178, 150, 135, 160]
-		},
-		{
-			label: '食品',
-			data: [114, 138, 200, 235, 190]
-		}
-	]
-};
-const options3 = {
-	type: 'pie',
-	title: {
-		text: '服装品类销售饼状图'
-	},
-	legend: {
-		position: 'left'
-	},
-	bgColor: '#fbfbfb',
-	labels: ['T恤', '牛仔裤', '连衣裙', '毛衣', '七分裤', '短裙', '羽绒服'],
-	datasets: [
-		{
-			data: [334, 278, 190, 235, 260, 200, 141]
-		}
-	]
-};
-const options4 = {
-	type: 'ring',
-	title: {
-		text: '环形三等分'
-	},
-	showValue: false,
-	legend: {
-		position: 'bottom',
-		bottom: 40
-	},
-	bgColor: '#fbfbfb',
-	labels: ['vue', 'react', 'angular'],
-	datasets: [
-		{
-			data: [500, 500, 500]
-		}
-	]
-};
-</script>
-
-<style scoped>
-.schart-box {
-	display: inline-block;
-	margin: 20px;
-}
-.schart {
-	width: 600px;
-	height: 400px;
-}
-.content-title {
-	clear: both;
-	font-weight: 400;
-	line-height: 50px;
-	margin: 10px 0;
-	font-size: 22px;
-	color: #1f2f3d;
-}
-</style>
+<template>
+	<div class="container">
+		<div class="plugins-tips">
+			vue-schart:vue.js封装sChart.js的图表组件。 访问地址:
+			<a href="https://github.com/lin-xin/vue-schart" target="_blank">vue-schart</a>
+		</div>
+		<el-card class="mgb20" shadow="hover">
+			<template #header>
+				<div class="content-title">柱状图</div>
+			</template>
+			<schart class="schart" canvasId="bar" :options="options1"></schart>
+		</el-card>
+		<el-card class="mgb20" shadow="hover">
+			<template #header>
+				<div class="content-title">折线图</div>
+			</template>
+			<schart class="schart" canvasId="line" :options="options2"></schart>
+		</el-card>
+		<el-card class="mgb20" shadow="hover">
+			<template #header>
+				<div class="content-title">饼状图</div>
+			</template>
+			<schart class="schart" canvasId="pie" :options="options3"></schart>
+		</el-card>
+		<el-card class="mgb20" shadow="hover">
+			<template #header>
+				<div class="content-title">环形图</div>
+			</template>
+			<schart class="schart" canvasId="ring" :options="options4"></schart>
+		</el-card>
+	</div>
+</template>
+
+<script setup lang="ts" name="schart">
+import Schart from 'vue-schart';
+
+const options1 = {
+	type: 'bar',
+	title: {
+		text: '最近一周各品类销售图'
+	},
+	colorList: ["#3f51b5", "#009688", "#f44336", "#00bcd4", "#1ABC9C"],
+	labels: ['周一', '周二', '周三', '周四', '周五'],
+	datasets: [
+		{
+			label: '家电',
+			// fillColor: 'rgba(241, 49, 74, 0.5)',
+			data: [234, 278, 270, 190, 230]
+		},
+		{
+			label: '百货',
+			data: [164, 178, 190, 135, 160]
+		},
+		{
+			label: '食品',
+			data: [144, 198, 150, 235, 120]
+		}
+	]
+};
+const options2 = {
+	type: 'line',
+	title: {
+		text: '最近几个月各品类销售趋势图'
+	},
+	colorList: ["#3f51b5", "#009688", "#f44336", "#00bcd4", "#1ABC9C"],
+	labels: ['6月', '7月', '8月', '9月', '10月'],
+	datasets: [
+		{
+			label: '家电',
+			data: [234, 278, 270, 190, 230]
+		},
+		{
+			label: '百货',
+			data: [164, 178, 150, 135, 160]
+		},
+		{
+			label: '食品',
+			data: [114, 138, 200, 235, 190]
+		}
+	]
+};
+const options3 = {
+	type: 'pie',
+	title: {
+		text: '服装品类销售饼状图'
+	},
+	legend: {
+		position: 'left'
+	},
+	colorList: ["#2196f3", '#673ab7', "#009688", "#1ABC9C", "#3f51b5", "#f44336", "#00bcd4"],
+	labels: ['T恤', '牛仔裤', '连衣裙', '毛衣', '七分裤', '短裙', '羽绒服'],
+	datasets: [
+		{
+			data: [334, 278, 190, 235, 260, 200, 141]
+		}
+	]
+};
+const options4 = {
+	type: 'ring',
+	title: {
+		text: '环形三等分'
+	},
+	showValue: false,
+	legend: {
+		position: 'bottom',
+		bottom: 40
+	},
+	colorList: ["#3f51b5", "#009688", "#f44336", "#00bcd4", "#1ABC9C"],
+	labels: ['vue', 'react', 'angular'],
+	datasets: [
+		{
+			data: [500, 500, 500]
+		}
+	]
+};
+</script>
+
+<style scoped>
+.schart {
+	width: 100%;
+	height: 400px;
+}
+
+.content-title {
+	font-weight: 400;
+	font-size: 22px;
+	color: #1f2f3d;
+}
+</style>

+ 357 - 301
src/views/dashboard.vue

@@ -1,301 +1,357 @@
-<template>
-	<div>
-		<el-row :gutter="20">
-			<el-col :span="8">
-				<el-card shadow="hover" class="mgb20" style="height: 252px">
-					<div class="user-info">
-						<el-avatar :size="120" :src="imgurl" />
-						<div class="user-info-cont">
-							<div class="user-info-name">{{ name }}</div>
-							<div>{{ role }}</div>
-						</div>
-					</div>
-					<div class="user-info-list">
-						上次登录时间:
-						<span>2022-10-01</span>
-					</div>
-					<div class="user-info-list">
-						上次登录地点:
-						<span>东莞</span>
-					</div>
-				</el-card>
-				<el-card shadow="hover" style="height: 252px">
-					<template #header>
-						<div class="clearfix">
-							<span>语言详情</span>
-						</div>
-					</template>
-					Vue
-					<el-progress :percentage="79.4" color="#42b983"></el-progress>
-					TypeScript
-					<el-progress :percentage="14" color="#f1e05a"></el-progress>
-					CSS
-					<el-progress :percentage="5.6"></el-progress>
-					HTML
-					<el-progress :percentage="1" color="#f56c6c"></el-progress>
-				</el-card>
-			</el-col>
-			<el-col :span="16">
-				<el-row :gutter="20" class="mgb20">
-					<el-col :span="8">
-						<el-card shadow="hover" :body-style="{ padding: '0px' }">
-							<div class="grid-content grid-con-1">
-								<el-icon class="grid-con-icon"><User /></el-icon>
-								<div class="grid-cont-right">
-									<div class="grid-num">1234</div>
-									<div>用户访问量</div>
-								</div>
-							</div>
-						</el-card>
-					</el-col>
-					<el-col :span="8">
-						<el-card shadow="hover" :body-style="{ padding: '0px' }">
-							<div class="grid-content grid-con-2">
-								<el-icon class="grid-con-icon"><ChatDotRound /></el-icon>
-								<div class="grid-cont-right">
-									<div class="grid-num">321</div>
-									<div>系统消息</div>
-								</div>
-							</div>
-						</el-card>
-					</el-col>
-					<el-col :span="8">
-						<el-card shadow="hover" :body-style="{ padding: '0px' }">
-							<div class="grid-content grid-con-3">
-								<el-icon class="grid-con-icon"><Goods /></el-icon>
-								<div class="grid-cont-right">
-									<div class="grid-num">5000</div>
-									<div>商品数量</div>
-								</div>
-							</div>
-						</el-card>
-					</el-col>
-				</el-row>
-				<el-card shadow="hover" style="height: 403px">
-					<template #header>
-						<div class="clearfix">
-							<span>待办事项</span>
-							<el-button style="float: right; padding: 3px 0" text>添加</el-button>
-						</div>
-					</template>
-
-					<el-table :show-header="false" :data="todoList" style="width: 100%">
-						<el-table-column width="40">
-							<template #default="scope">
-								<el-checkbox v-model="scope.row.status"></el-checkbox>
-							</template>
-						</el-table-column>
-						<el-table-column>
-							<template #default="scope">
-								<div
-									class="todo-item"
-									:class="{
-										'todo-item-del': scope.row.status
-									}"
-								>
-									{{ scope.row.title }}
-								</div>
-							</template>
-						</el-table-column>
-					</el-table>
-				</el-card>
-			</el-col>
-		</el-row>
-		<el-row :gutter="20">
-			<el-col :span="12">
-				<el-card shadow="hover">
-					<schart ref="bar" class="schart" canvasId="bar" :options="options"></schart>
-				</el-card>
-			</el-col>
-			<el-col :span="12">
-				<el-card shadow="hover">
-					<schart ref="line" class="schart" canvasId="line" :options="options2"></schart>
-				</el-card>
-			</el-col>
-		</el-row>
-	</div>
-</template>
-
-<script setup lang="ts" name="dashboard">
-import Schart from 'vue-schart';
-import { reactive } from 'vue';
-import imgurl from '../assets/img/img.jpg';
-
-const name = localStorage.getItem('ms_username');
-const role: string = name === 'admin' ? '超级管理员' : '普通用户';
-
-const options = {
-	type: 'bar',
-	title: {
-		text: '最近一周各品类销售图'
-	},
-	xRorate: 25,
-	labels: ['周一', '周二', '周三', '周四', '周五'],
-	datasets: [
-		{
-			label: '家电',
-			data: [234, 278, 270, 190, 230]
-		},
-		{
-			label: '百货',
-			data: [164, 178, 190, 135, 160]
-		},
-		{
-			label: '食品',
-			data: [144, 198, 150, 235, 120]
-		}
-	]
-};
-const options2 = {
-	type: 'line',
-	title: {
-		text: '最近几个月各品类销售趋势图'
-	},
-	labels: ['6月', '7月', '8月', '9月', '10月'],
-	datasets: [
-		{
-			label: '家电',
-			data: [234, 278, 270, 190, 230]
-		},
-		{
-			label: '百货',
-			data: [164, 178, 150, 135, 160]
-		},
-		{
-			label: '食品',
-			data: [74, 118, 200, 235, 90]
-		}
-	]
-};
-const todoList = reactive([
-	{
-		title: '今天要修复100个bug',
-		status: false
-	},
-	{
-		title: '今天要修复100个bug',
-		status: false
-	},
-	{
-		title: '今天要写100行代码加几个bug吧',
-		status: false
-	},
-	{
-		title: '今天要修复100个bug',
-		status: false
-	},
-	{
-		title: '今天要修复100个bug',
-		status: true
-	},
-	{
-		title: '今天要写100行代码加几个bug吧',
-		status: true
-	}
-]);
-</script>
-
-<style scoped>
-.el-row {
-	margin-bottom: 20px;
-}
-
-.grid-content {
-	display: flex;
-	align-items: center;
-	height: 100px;
-}
-
-.grid-cont-right {
-	flex: 1;
-	text-align: center;
-	font-size: 14px;
-	color: #999;
-}
-
-.grid-num {
-	font-size: 30px;
-	font-weight: bold;
-}
-
-.grid-con-icon {
-	font-size: 50px;
-	width: 100px;
-	height: 100px;
-	text-align: center;
-	line-height: 100px;
-	color: #fff;
-}
-
-.grid-con-1 .grid-con-icon {
-	background: rgb(45, 140, 240);
-}
-
-.grid-con-1 .grid-num {
-	color: rgb(45, 140, 240);
-}
-
-.grid-con-2 .grid-con-icon {
-	background: rgb(100, 213, 114);
-}
-
-.grid-con-2 .grid-num {
-	color: rgb(100, 213, 114);
-}
-
-.grid-con-3 .grid-con-icon {
-	background: rgb(242, 94, 67);
-}
-
-.grid-con-3 .grid-num {
-	color: rgb(242, 94, 67);
-}
-
-.user-info {
-	display: flex;
-	align-items: center;
-	padding-bottom: 20px;
-	border-bottom: 2px solid #ccc;
-	margin-bottom: 20px;
-}
-
-.user-info-cont {
-	padding-left: 50px;
-	flex: 1;
-	font-size: 14px;
-	color: #999;
-}
-
-.user-info-cont div:first-child {
-	font-size: 30px;
-	color: #222;
-}
-
-.user-info-list {
-	font-size: 14px;
-	color: #999;
-	line-height: 25px;
-}
-
-.user-info-list span {
-	margin-left: 70px;
-}
-
-.mgb20 {
-	margin-bottom: 20px;
-}
-
-.todo-item {
-	font-size: 14px;
-}
-
-.todo-item-del {
-	text-decoration: line-through;
-	color: #999;
-}
-
-.schart {
-	width: 100%;
-	height: 300px;
-}
-</style>
+<template>
+    <div>
+        <el-row :gutter="20" class="mgb20">
+            <el-col :span="6">
+                <el-card shadow="hover" body-class="card-body">
+                    <el-icon class="card-icon bg1">
+                        <User />
+                    </el-icon>
+                    <div class="card-content">
+                        <countup class="card-num color1" :end="6666" />
+                        <div>用户访问量</div>
+                    </div>
+                </el-card>
+            </el-col>
+            <el-col :span="6">
+                <el-card shadow="hover" body-class="card-body">
+                    <el-icon class="card-icon bg2">
+                        <ChatDotRound />
+                    </el-icon>
+                    <div class="card-content">
+                        <countup class="card-num color2" :end="168" />
+                        <div>系统消息</div>
+                    </div>
+                </el-card>
+            </el-col>
+            <el-col :span="6">
+                <el-card shadow="hover" body-class="card-body">
+                    <el-icon class="card-icon bg3">
+                        <Goods />
+                    </el-icon>
+                    <div class="card-content">
+                        <countup class="card-num color3" :end="8888" />
+                        <div>商品数量</div>
+                    </div>
+                </el-card>
+            </el-col>
+            <el-col :span="6">
+                <el-card shadow="hover" body-class="card-body">
+                    <el-icon class="card-icon bg4">
+                        <ShoppingCartFull />
+                    </el-icon>
+                    <div class="card-content">
+                        <countup class="card-num color4" :end="568" />
+                        <div>今日订单量</div>
+                    </div>
+                </el-card>
+            </el-col>
+        </el-row>
+
+        <el-row :gutter="20" class="mgb20">
+            <el-col :span="18">
+                <el-card shadow="hover">
+                    <div class="card-header">
+                        <p class="card-header-title">订单动态</p>
+                        <p class="card-header-desc">最近一周订单状态,包括订单成交量和订单退货量</p>
+                    </div>
+                    <v-chart class="chart" :option="dashOpt1" />
+                </el-card>
+            </el-col>
+            <el-col :span="6">
+                <el-card shadow="hover">
+                    <div class="card-header">
+                        <p class="card-header-title">品类分布</p>
+                        <p class="card-header-desc">最近一个月销售商品的品类情况</p>
+                    </div>
+                    <v-chart class="chart" :option="dashOpt2" />
+                </el-card>
+            </el-col>
+        </el-row>
+        <el-row :gutter="20">
+            <el-col :span="7">
+                <el-card shadow="hover" :body-style="{ height: '400px' }">
+                    <div class="card-header">
+                        <p class="card-header-title">时间线</p>
+                        <p class="card-header-desc">最新的销售动态和活动信息</p>
+                    </div>
+                    <el-timeline>
+                        <el-timeline-item v-for="(activity, index) in activities" :key="index" :color="activity.color">
+                            <div class="timeline-item">
+                                <div>
+                                    <p>{{ activity.content }}</p>
+                                    <p class="timeline-desc">{{ activity.description }}</p>
+                                </div>
+                                <div class="timeline-time">{{ activity.timestamp }}</div>
+                            </div>
+                        </el-timeline-item>
+                    </el-timeline>
+                </el-card>
+            </el-col>
+            <el-col :span="10">
+                <el-card shadow="hover" :body-style="{ height: '400px' }">
+                    <div class="card-header">
+                        <p class="card-header-title">渠道统计</p>
+                        <p class="card-header-desc">最近一个月的订单来源统计</p>
+                    </div>
+                    <v-chart class="map-chart" :option="mapOptions" />
+                </el-card>
+            </el-col>
+            <el-col :span="7">
+                <el-card shadow="hover" :body-style="{ height: '400px' }">
+                    <div class="card-header">
+                        <p class="card-header-title">排行榜</p>
+                        <p class="card-header-desc">销售商品的热门榜单Top5</p>
+                    </div>
+                    <div>
+                        <div class="rank-item" v-for="(rank, index) in ranks">
+                            <div class="rank-item-avatar">{{ index + 1 }}</div>
+                            <div class="rank-item-content">
+                                <div class="rank-item-top">
+                                    <div class="rank-item-title">{{ rank.title }}</div>
+                                    <div class="rank-item-desc">销量:{{ rank.value }}</div>
+                                </div>
+                                <el-progress
+                                    :show-text="false"
+                                    striped
+                                    :stroke-width="10"
+                                    :percentage="rank.percent"
+                                    :color="rank.color"
+                                />
+                            </div>
+                        </div>
+                    </div>
+                </el-card>
+            </el-col>
+        </el-row>
+    </div>
+</template>
+
+<script setup lang="ts" name="dashboard">
+import countup from '@/components/countup.vue';
+import { use, registerMap } from 'echarts/core';
+import { BarChart, LineChart, PieChart, MapChart } from 'echarts/charts';
+import {
+    GridComponent,
+    TooltipComponent,
+    LegendComponent,
+    TitleComponent,
+    VisualMapComponent,
+} from 'echarts/components';
+import { CanvasRenderer } from 'echarts/renderers';
+import VChart from 'vue-echarts';
+import { dashOpt1, dashOpt2, mapOptions } from './chart/options';
+import chinaMap from '@/utils/china';
+use([
+    CanvasRenderer,
+    BarChart,
+    GridComponent,
+    LineChart,
+    PieChart,
+    TooltipComponent,
+    LegendComponent,
+    TitleComponent,
+    VisualMapComponent,
+    MapChart,
+]);
+registerMap('china', chinaMap);
+const activities = [
+    {
+        content: '收藏商品',
+        description: 'xxx收藏了你的商品,就是不买',
+        timestamp: '30分钟前',
+        color: '#00bcd4',
+    },
+    {
+        content: '用户评价',
+        description: 'xxx给了某某商品一个差评,吐血啊',
+        timestamp: '55分钟前',
+        color: '#1ABC9C',
+    },
+    {
+        content: '订单提交',
+        description: 'xxx提交了订单,快去收钱吧',
+        timestamp: '1小时前',
+        color: '#3f51b5',
+    },
+    {
+        content: '退款申请',
+        description: 'xxx申请了仅退款,又要亏钱了',
+        timestamp: '15小时前',
+        color: '#f44336',
+    },
+    {
+        content: '商品上架',
+        description: '运营专员瞒着你上架了一辆飞机',
+        timestamp: '1天前',
+        color: '#009688',
+    },
+];
+
+const ranks = [
+    {
+        title: '手机',
+        value: 10000,
+        percent: 80,
+        color: '#f25e43',
+    },
+    {
+        title: '电脑',
+        value: 8000,
+        percent: 70,
+        color: '#00bcd4',
+    },
+    {
+        title: '相机',
+        value: 6000,
+        percent: 60,
+        color: '#64d572',
+    },
+    {
+        title: '衣服',
+        value: 5000,
+        percent: 55,
+        color: '#e9a745',
+    },
+    {
+        title: '书籍',
+        value: 4000,
+        percent: 50,
+        color: '#009688',
+    },
+];
+</script>
+
+<style>
+.card-body {
+    display: flex;
+    align-items: center;
+    height: 100px;
+    padding: 0;
+}
+</style>
+<style scoped>
+.card-content {
+    flex: 1;
+    text-align: center;
+    font-size: 14px;
+    color: #999;
+    padding: 0 20px;
+}
+
+.card-num {
+    font-size: 30px;
+}
+
+.card-icon {
+    font-size: 50px;
+    width: 100px;
+    height: 100px;
+    text-align: center;
+    line-height: 100px;
+    color: #fff;
+}
+
+.bg1 {
+    background: #2d8cf0;
+}
+
+.bg2 {
+    background: #64d572;
+}
+
+.bg3 {
+    background: #f25e43;
+}
+
+.bg4 {
+    background: #e9a745;
+}
+
+.color1 {
+    color: #2d8cf0;
+}
+
+.color2 {
+    color: #64d572;
+}
+
+.color3 {
+    color: #f25e43;
+}
+
+.color4 {
+    color: #e9a745;
+}
+
+.chart {
+    width: 100%;
+    height: 400px;
+}
+
+.card-header {
+    padding-left: 10px;
+    margin-bottom: 20px;
+}
+
+.card-header-title {
+    font-size: 18px;
+    font-weight: bold;
+    margin-bottom: 5px;
+}
+
+.card-header-desc {
+    font-size: 14px;
+    color: #999;
+}
+
+.timeline-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: 16px;
+    color: #000;
+}
+
+.timeline-time,
+.timeline-desc {
+    font-size: 12px;
+    color: #787878;
+}
+
+.rank-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 20px;
+}
+
+.rank-item-avatar {
+    width: 40px;
+    height: 40px;
+    border-radius: 50%;
+    background: #f2f2f2;
+    text-align: center;
+    line-height: 40px;
+    margin-right: 10px;
+}
+
+.rank-item-content {
+    flex: 1;
+}
+
+.rank-item-top {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    color: #343434;
+    margin-bottom: 10px;
+}
+
+.rank-item-desc {
+    font-size: 14px;
+    color: #999;
+}
+.map-chart {
+    width: 100%;
+    height: 350px;
+}
+</style>

+ 0 - 14
src/views/donate.vue

@@ -1,14 +0,0 @@
-<template>
-	<div class="container">
-		<div class="plugins-tips">
-			如果该框架对你有帮助,那就请作者喝杯饮料吧!<el-icon><ColdDrink /></el-icon> 加微信号linxin_20探讨问题。
-		</div>
-		<div>
-			<img src="https://lin-xin.gitee.io/images/weixin.jpg" />
-		</div>
-	</div>
-</template>
-
-<script setup lang="ts" name="donate"></script>
-
-<style></style>

+ 82 - 0
src/views/element/calendar.vue

@@ -0,0 +1,82 @@
+<template>
+    <div class="container">
+        <el-calendar v-model="value">
+            <template #date-cell="{ data }">
+                <div>{{ data.date.getDate() }}</div>
+                <div class="notes-container" v-if="notes[data.day.toString()]">
+                    <div class="notes" v-for="note in notes[data.day.toString()]">
+                        <span :class="note.status === 1 ? 'text-success' : 'text-danger'"></span>
+                        <div class="note-title">{{ note.title }}</div>
+                    </div>
+                </div>
+            </template>
+        </el-calendar>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+const today = new Date();
+const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
+const value = ref(today);
+
+const todayDate = today.toISOString().slice(0, 10);
+const yesterdayDate = yesterday.toISOString().slice(0, 10);
+
+const notes: any = {
+    [todayDate]: [
+        { title: '吃饭', status: 1 },
+        { title: '睡觉', status: 0 },
+        { title: '吃饭', status: 1 },
+        { title: '睡觉', status: 0 },
+        { title: '吃饭', status: 1 },
+        { title: '睡觉', status: 0 },
+    ],
+    [yesterdayDate]: [{ title: '参加会议', status: 0 }],
+};
+</script>
+
+<style scoped>
+.notes-container {
+    height: 60px;
+    overflow-y: auto;
+}
+
+.notes-container::-webkit-scrollbar {
+    width: 0;
+}
+
+.notes {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    font-size: 12px;
+}
+
+.notes:hover {
+    background-color: #eee;
+}
+
+.note-title {
+    flex: 1;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+}
+
+.notes span {
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    margin-right: 5px;
+}
+
+.text-success {
+    background-color: #5cb85c;
+}
+
+.text-danger {
+    background-color: #d9534f;
+}
+</style>

+ 66 - 0
src/views/element/carousel.vue

@@ -0,0 +1,66 @@
+<template>
+    <div>
+        <el-card class="mgb20">
+            <template #header>基础用法</template>
+            <el-carousel height="400px">
+                <el-carousel-item v-for="item in 4" :key="item">
+                    <h3>{{ item }}</h3>
+                </el-carousel-item>
+            </el-carousel>
+        </el-card>
+
+        <el-row :gutter="20">
+            <el-col :span="12">
+                <el-card class="mgb20">
+                    <template #header>轮播图</template>
+                    <el-carousel height="300px">
+                        <el-carousel-item v-for="item in imgs" :key="item">
+                            <el-image class="carousel-img" :src="item" fit="cover" />
+                        </el-carousel-item>
+                    </el-carousel>
+                </el-card>
+            </el-col>
+            <el-col :span="12">
+                <el-card class="mgb20">
+                    <template #header>卡片模式</template>
+                    <el-carousel height="300px" type="card">
+                        <el-carousel-item v-for="item in imgs" :key="item">
+                            <el-image class="carousel-img" :src="item" fit="cover" />
+                        </el-carousel-item>
+                    </el-carousel>
+                </el-card>
+            </el-col>
+        </el-row>
+    </div>
+</template>
+
+<script lang="ts" setup>
+const imgs = [
+    'https://cdn.pixabay.com/photo/2017/08/07/08/23/sea-2601374_640.jpg',
+    'https://cdn.pixabay.com/photo/2020/02/11/10/24/lake-4839058_640.jpg',
+    'https://cdn.pixabay.com/photo/2024/02/21/08/06/coast-8587004_640.jpg',
+    'https://cdn.pixabay.com/photo/2023/07/29/10/21/grasshopper-8156626_640.jpg',
+];
+</script>
+
+<style scoped>
+.el-carousel__item h3 {
+    color: #475669;
+    line-height: 400px;
+    margin: 0;
+    text-align: center;
+}
+
+.el-carousel__item:nth-child(2n) {
+    background-color: #99a9bf;
+}
+
+.el-carousel__item:nth-child(2n + 1) {
+    background-color: #d3dce6;
+}
+
+.carousel-img {
+    width: 100%;
+    height: 100%;
+}
+</style>

+ 189 - 0
src/views/element/form.vue

@@ -0,0 +1,189 @@
+<template>
+    <div class="container">
+        <el-radio-group class="mgb20" v-model="labelPosition">
+            <el-radio-button value="left">Left</el-radio-button>
+            <el-radio-button value="right">Right</el-radio-button>
+            <el-radio-button value="top">Top</el-radio-button>
+        </el-radio-group>
+        <el-form ref="formRef" :rules="rules" :model="form" label-width="120px" :label-position="labelPosition">
+            <el-row :gutter="50">
+                <el-col :span="10">
+                    <el-form-item label="文本框" prop="name">
+                        <el-input v-model="form.name"></el-input>
+                    </el-form-item>
+                    <el-form-item label="数字框" prop="num">
+                        <el-input-number v-model="form.num" :min="1" :max="10" />
+                    </el-form-item>
+                    <el-form-item label="日期选择" prop="date">
+                        <el-date-picker type="date" placeholder="选择日期" v-model="form.date"></el-date-picker>
+                    </el-form-item>
+                    <el-form-item label="时间选择" prop="time">
+                        <el-time-picker placeholder="选择时间" v-model="form.time">
+                        </el-time-picker>
+                    </el-form-item>
+                    <el-form-item label="选择器" prop="region">
+                        <el-select v-model="form.region" placeholder="请选择">
+                            <el-option key="小明" label="小明" value="小明"></el-option>
+                            <el-option key="小红" label="小红" value="小红"></el-option>
+                            <el-option key="小白" label="小白" value="小白"></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="城市级联" prop="options">
+                        <el-cascader :options="options" v-model="form.options"></el-cascader>
+                    </el-form-item>
+                    <el-form-item label="文本框" prop="desc">
+                        <el-input type="textarea" rows="5" v-model="form.desc"></el-input>
+                    </el-form-item>
+                </el-col>
+                <el-col :span="12">
+                    <el-form-item label="评分" prop="rate">
+                        <el-rate v-model="form.rate" allow-half />
+                    </el-form-item>
+                    <el-form-item label="滑块" prop="num">
+                        <el-slider v-model="form.num" :step="1" show-stops :max="10" />
+                    </el-form-item>
+                    <el-form-item label="开关" prop="delivery">
+                        <el-switch v-model="form.delivery"></el-switch>
+                    </el-form-item>
+                    <el-form-item label="颜色选择" prop="color">
+                        <el-color-picker v-model="form.color" />
+                    </el-form-item>
+                    <el-form-item label="多选框" prop="type">
+                        <el-checkbox-group v-model="form.type">
+                            <el-checkbox label="小明" value="小明" name="type"></el-checkbox>
+                            <el-checkbox label="小红" value="小红" name="type"></el-checkbox>
+                            <el-checkbox label="小白" value="小白" name="type"></el-checkbox>
+                        </el-checkbox-group>
+                    </el-form-item>
+                    <el-form-item label="单选框" prop="resource">
+                        <el-radio-group v-model="form.resource">
+                            <el-radio label="小明" value="小明"></el-radio>
+                            <el-radio label="小红" value="小红"></el-radio>
+                            <el-radio label="小白" value="小白"></el-radio>
+                        </el-radio-group>
+                    </el-form-item>
+                    <el-form-item label="穿梭框" prop="transfer">
+                        <el-transfer v-model="form.transfer" :data="transferData" />
+                    </el-form-item>
+                </el-col>
+
+                <el-col :span="24">
+                    <el-form-item>
+                        <el-button type="primary" @click="onSubmit(formRef)">表单提交</el-button>
+                        <el-button @click="onReset(formRef)">重置表单</el-button>
+                    </el-form-item>
+                </el-col>
+            </el-row>
+        </el-form>
+    </div>
+</template>
+
+<script setup lang="ts" name="forms">
+import { reactive, ref } from 'vue';
+import { ElMessage } from 'element-plus';
+import type { FormInstance, FormProps, FormRules } from 'element-plus';
+const labelPosition = ref<FormProps['labelPosition']>('right')
+const options = [
+    {
+        value: 'guangdong',
+        label: '广东省',
+        children: [
+            {
+                value: 'guangzhou',
+                label: '广州市',
+                children: [
+                    {
+                        value: 'tianhe',
+                        label: '天河区',
+                    },
+                    {
+                        value: 'haizhu',
+                        label: '海珠区',
+                    },
+                ],
+            },
+            {
+                value: 'dongguan',
+                label: '东莞市',
+                children: [
+                    {
+                        value: 'changan',
+                        label: '长安镇',
+                    },
+                    {
+                        value: 'humen',
+                        label: '虎门镇',
+                    },
+                ],
+            },
+        ],
+    },
+    {
+        value: 'hunan',
+        label: '湖南省',
+        children: [
+            {
+                value: 'changsha',
+                label: '长沙市',
+                children: [
+                    {
+                        value: 'yuelu',
+                        label: '岳麓区',
+                    },
+                ],
+            },
+        ],
+    },
+];
+const rules: FormRules = {
+    name: [{ required: true, message: '请输入表单名称', trigger: 'blur' }],
+};
+const formRef = ref<FormInstance>();
+const form = reactive({
+    name: '',
+    region: '',
+    date: '',
+    time: '',
+    delivery: true,
+    type: ['小明'],
+    resource: '小红',
+    desc: '',
+    options: [],
+    color: '',
+    num: 1,
+    rate: 0,
+    transfer: [],
+
+});
+const generateData = () => {
+    const data = []
+    for (let i = 1; i <= 15; i++) {
+        data.push({
+            key: i,
+            label: `Option ${i}`,
+            disabled: i % 4 === 0,
+        })
+    }
+    return data
+}
+
+const transferData = ref(generateData())
+// 提交
+const onSubmit = (formEl: FormInstance | undefined) => {
+    // 表单校验
+    if (!formEl) return;
+    formEl.validate((valid) => {
+        if (valid) {
+            console.log(form);
+            ElMessage.success('提交成功!');
+        } else {
+            return false;
+        }
+    });
+};
+// 重置
+const onReset = (formEl: FormInstance | undefined) => {
+    if (!formEl) return;
+    formEl.resetFields();
+};
+</script>

+ 340 - 0
src/views/element/statistic.vue

@@ -0,0 +1,340 @@
+<template>
+    <div>
+
+
+        <el-card class="mgb20" shadow="hover">
+            <template #header>基础用法</template>
+            <el-row>
+                <el-col :span="6" style="text-align: center">
+                    <el-statistic title="Daily active users" :value="268500" />
+                </el-col>
+                <el-col :span="6" style="text-align: center">
+                    <el-statistic :value="138">
+                        <template #title>
+                            <div style="display: inline-flex; align-items: center">
+                                Ratio of men to women
+                            </div>
+                        </template>
+                        <template #suffix>/100</template>
+                    </el-statistic>
+                </el-col>
+                <el-col :span="6" style="text-align: center">
+                    <el-statistic title="数字滚动" :value="outputValue" />
+                </el-col>
+                <el-col :span="6" style="text-align: center">
+                    <el-countdown title="倒计时" :value="value" />
+                </el-col>
+            </el-row>
+        </el-card>
+
+        <el-card class="mgb20" shadow="hover">
+            <template #header>CountUp.js</template>
+            <div class="plugins-tips">
+                countup.js:用于快速创建以更有趣的方式显示数字数据的动画。 访问地址:
+                <a href="https://github.com/inorganik/countUp.js" target="_blank">countUp.js</a>
+            </div>
+            <el-row>
+                <el-col :span="8" style="text-align: center">
+                    <p>基础用法</p>
+                    <countup class="countup" :end="6666" />
+                </el-col>
+                <el-col :span="8" style="text-align: center">
+                    <p>具体配置</p>
+                    <countup class="countup" :end="8888.5" :options="options" />
+                </el-col>
+                <el-col :span="8" style="text-align: center">
+                    <p>更新数值</p>
+                    <countup class="countup" :end="value1" />
+                </el-col>
+            </el-row>
+        </el-card>
+        <el-card class="mgb20" shadow="never">
+            <template #header>统计卡片</template>
+
+            <el-row :gutter="20" class="mgb20">
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <el-icon class="card-icon color1">
+                            <User />
+                        </el-icon>
+                        <div class="card-content text-right">
+                            <el-statistic title="日活跃用户量" :value="268500" />
+                        </div>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <el-icon class="card-icon color2">
+                            <ChatDotRound />
+                        </el-icon>
+                        <div class="card-content text-right">
+                            <el-statistic title="系统消息" :value="16800" />
+                        </div>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <el-icon class="card-icon color3">
+                            <Goods />
+                        </el-icon>
+                        <div class="card-content text-right">
+                            <el-statistic title="商品数量" :value="8888" />
+                        </div>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <el-icon class="card-icon color4">
+                            <ShoppingCartFull />
+                        </el-icon>
+                        <div class="card-content text-right">
+                            <el-statistic title="今日订单量" :value="56888" />
+                        </div>
+                    </el-card>
+                </el-col>
+            </el-row>
+            <el-row :gutter="20" class="mgb20">
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <div class="card-content text-left">
+                            <el-statistic :value-style="{ color: '#2d8cf0' }" title="日活跃用户量" :value="268500" />
+                        </div>
+                        <el-icon class="card-icon color1">
+                            <User />
+                        </el-icon>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <div class="card-content text-left">
+                            <el-statistic :value-style="{ color: '#64d572' }" title="系统消息" :value="16800" />
+                        </div>
+                        <el-icon class="card-icon color2">
+                            <ChatDotRound />
+                        </el-icon>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <div class="card-content text-left">
+                            <el-statistic :value-style="{ color: '#f25e43' }" title="商品数量" :value="8888" />
+                        </div>
+                        <el-icon class="card-icon color3">
+                            <Goods />
+                        </el-icon>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <div class="card-content text-left">
+                            <el-statistic :value-style="{ color: '#e9a745' }" title="今日订单量" :value="56888" />
+                        </div>
+                        <el-icon class="card-icon color4">
+                            <ShoppingCartFull />
+                        </el-icon>
+                    </el-card>
+                </el-col>
+            </el-row>
+            <el-row :gutter="20" class="mgb20">
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <el-icon class="card-icon bg1">
+                            <User />
+                        </el-icon>
+                        <div class="card-content">
+                            <countup class="card-num color1" :end="6666" />
+                            <div>用户访问量</div>
+                        </div>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <el-icon class="card-icon bg2">
+                            <ChatDotRound />
+                        </el-icon>
+                        <div class="card-content">
+                            <countup class="card-num color2" :end="168" />
+                            <div>系统消息</div>
+                        </div>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <el-icon class="card-icon bg3">
+                            <Goods />
+                        </el-icon>
+                        <div class="card-content">
+                            <countup class="card-num color3" :end="8888" />
+                            <div>商品数量</div>
+                        </div>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body">
+                        <el-icon class="card-icon bg4">
+                            <ShoppingCartFull />
+                        </el-icon>
+                        <div class="card-content">
+                            <countup class="card-num color4" :end="568" />
+                            <div>今日订单量</div>
+                        </div>
+                    </el-card>
+                </el-col>
+            </el-row>
+            <el-row :gutter="20" class="mgb20">
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body bg1">
+                        <el-icon class="card-icon ">
+                            <User />
+                        </el-icon>
+                        <div class="card-content color0">
+                            <countup class="card-num" :end="6666" />
+                            <div>用户访问量</div>
+                        </div>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body bg2">
+                        <el-icon class="card-icon">
+                            <ChatDotRound />
+                        </el-icon>
+                        <div class="card-content color0">
+                            <countup class="card-num" :end="168" />
+                            <div>系统消息</div>
+                        </div>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body bg3">
+                        <el-icon class="card-icon">
+                            <Goods />
+                        </el-icon>
+                        <div class="card-content color0">
+                            <countup class="card-num " :end="8888" />
+                            <div>商品数量</div>
+                        </div>
+                    </el-card>
+                </el-col>
+                <el-col :span="6">
+                    <el-card shadow="hover" body-class="card-body bg4">
+                        <el-icon class="card-icon">
+                            <ShoppingCartFull />
+                        </el-icon>
+                        <div class="card-content color0">
+                            <countup class="card-num " :end="568" />
+                            <div>今日订单量</div>
+                        </div>
+                    </el-card>
+                </el-col>
+            </el-row>
+        </el-card>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { useTransition } from '@vueuse/core'
+import countup from '@/components/countup.vue';
+
+const source = ref(0)
+const outputValue = useTransition(source, {
+    duration: 1500,
+})
+source.value = 172000
+
+const value = ref(Date.now() + 1000 * 60 * 60 * 7)
+const value1 = ref(1000);
+setTimeout(() => {
+    value1.value = 8000;
+}, 5000);
+const options = {
+    startVal: 1000,
+    decimalPlaces: 2,
+    duration: 5,
+    useGrouping: false,
+    prefix: '$',
+    separator: ',',
+    decimal: '.',
+    suffix: '',
+}
+</script>
+
+<style>
+.card-body {
+    display: flex;
+    align-items: center;
+    height: 100px;
+    padding: 0;
+}
+
+.bg1 {
+    background: #2d8cf0;
+}
+
+.bg2 {
+    background: #64d572;
+}
+
+.bg3 {
+    background: #f25e43;
+}
+
+.bg4 {
+    background: #e9a745;
+}
+</style>
+<style scoped>
+.countup {
+    font-size: 24px;
+}
+
+.card-content {
+    flex: 1;
+    text-align: center;
+    font-size: 14px;
+    color: #999;
+    padding: 0 20px;
+}
+
+.card-num {
+    font-size: 30px;
+}
+
+.card-icon {
+    font-size: 50px;
+    width: 100px;
+    height: 100px;
+    text-align: center;
+    line-height: 100px;
+    color: #fff;
+}
+
+
+.color0 {
+    color: #fff;
+}
+
+.color1 {
+    color: #2d8cf0;
+}
+
+.color2 {
+    color: #64d572;
+}
+
+.color3 {
+    color: #f25e43;
+}
+
+.color4 {
+    color: #e9a745;
+}
+
+.text-right {
+    text-align: right;
+}
+
+.text-left {
+    text-align: left;
+}
+</style>

+ 61 - 0
src/views/element/steps.vue

@@ -0,0 +1,61 @@
+<template>
+    <div class="container">
+        <div class="step-div" v-if="step === 0">
+            <p>输入注册时的邮箱,我们会发送验证码到您的邮箱</p>
+            <el-input placeholder="请输入邮箱"></el-input>
+            <el-button class="step-btn" type="primary" @click="step++">下一步</el-button>
+        </div>
+        <div class="step-div" v-else-if="step === 1">
+            <p>验证码已发送至您的邮箱,请输入验证码</p>
+            <el-input placeholder="请输入验证码"></el-input>
+            <el-button class="step-btn" type="primary" @click="step++">下一步</el-button>
+        </div>
+
+        <div class="step-div" v-else-if="step === 2">
+            <p>请输入6位以上密码</p>
+            <el-input placeholder="请输入新密码"></el-input>
+            <el-button class="step-btn" type="primary" @click="step++">保存</el-button>
+        </div>
+        <div v-else>
+            <el-result icon="success" title="保存成功" sub-title="请退出后重新登录"></el-result>
+        </div>
+        <el-steps class="step-style" :active="step" align-center finish-status="success">
+            <el-step title="Step 1" description="填写邮箱" />
+            <el-step title="Step 2" description="填写验证码" />
+            <el-step title="Step 3" description="修改密码" />
+        </el-steps>
+        <el-steps class="step-style" :active="step" finish-status="success" simple>
+            <el-step title="填写邮箱" />
+            <el-step title="填写验证码" />
+            <el-step title="修改密码" />
+        </el-steps>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+const step = ref(0)
+</script>
+
+<style scoped>
+.step-div {
+    max-width: 500px;
+    margin: 0 auto;
+}
+
+.step-div p {
+    margin-bottom: 20px;
+    color: #787878;
+}
+
+.step-btn {
+    display: block;
+    width: 100%;
+    margin: 20px 0;
+}
+
+.step-style {
+    max-width: 800px;
+    margin: 40px auto;
+}
+</style>

+ 116 - 0
src/views/element/tabs.vue

@@ -0,0 +1,116 @@
+<template>
+	<el-tabs v-model="message" type="card">
+		<el-tab-pane :label="`未读消息(${state.unread.length})`" name="first">
+			<el-table :data="state.unread" :show-header="false" style="width: 100%">
+				<el-table-column>
+					<template #default="scope">
+						<span class="message-title">{{ scope.row.title }}</span>
+					</template>
+				</el-table-column>
+				<el-table-column prop="date" width="180"></el-table-column>
+				<el-table-column width="120">
+					<template #default="scope">
+						<el-button size="small" @click="handleRead(scope.$index)">标为已读</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<div class="handle-row">
+				<el-button type="primary">全部标为已读</el-button>
+			</div>
+		</el-tab-pane>
+		<el-tab-pane :label="`已读消息(${state.read.length})`" name="second">
+			<template v-if="message === 'second'">
+				<el-table :data="state.read" :show-header="false" style="width: 100%">
+					<el-table-column>
+						<template #default="scope">
+							<span class="message-title">{{ scope.row.title }}</span>
+						</template>
+					</el-table-column>
+					<el-table-column prop="date" width="180"></el-table-column>
+					<el-table-column width="120">
+						<template #default="scope">
+							<el-button type="danger" size="small" @click="handleDel(scope.$index)">删除</el-button>
+						</template>
+					</el-table-column>
+				</el-table>
+				<div class="handle-row">
+					<el-button type="danger">删除全部</el-button>
+				</div>
+			</template>
+		</el-tab-pane>
+		<el-tab-pane :label="`回收站(${state.recycle.length})`" name="third">
+			<template v-if="message === 'third'">
+				<el-table :data="state.recycle" :show-header="false" style="width: 100%">
+					<el-table-column>
+						<template #default="scope">
+							<span class="message-title">{{ scope.row.title }}</span>
+						</template>
+					</el-table-column>
+					<el-table-column prop="date" width="180"></el-table-column>
+					<el-table-column width="120">
+						<template #default="scope">
+							<el-button size="small" @click="handleRestore(scope.$index)">还原</el-button>
+						</template>
+					</el-table-column>
+				</el-table>
+				<div class="handle-row">
+					<el-button type="danger">清空回收站</el-button>
+				</div>
+			</template>
+		</el-tab-pane>
+	</el-tabs>
+</template>
+
+<script setup lang="ts" name="tabs">
+import { ref, reactive } from 'vue';
+
+const message = ref('first');
+const state = reactive({
+	unread: [
+		{
+			date: '2018-04-19 20:00:00',
+			title: '【系统通知】该系统将于今晚凌晨2点到5点进行升级维护'
+		},
+		{
+			date: '2018-04-19 21:00:00',
+			title: '今晚12点整发大红包,先到先得'
+		}
+	],
+	read: [
+		{
+			date: '2018-04-19 20:00:00',
+			title: '【系统通知】该系统将于今晚凌晨2点到5点进行升级维护'
+		}
+	],
+	recycle: [
+		{
+			date: '2018-04-19 20:00:00',
+			title: '【系统通知】该系统将于今晚凌晨2点到5点进行升级维护'
+		}
+	]
+});
+
+const handleRead = (index: number) => {
+	const item = state.unread.splice(index, 1);
+	state.read = item.concat(state.read);
+};
+const handleDel = (index: number) => {
+	const item = state.read.splice(index, 1);
+	state.recycle = item.concat(state.recycle);
+};
+const handleRestore = (index: number) => {
+	const item = state.recycle.splice(index, 1);
+	state.read = item.concat(state.read);
+};
+</script>
+
+<style>
+.message-title {
+	cursor: pointer;
+	color: var(--el-color-primary);
+}
+
+.handle-row {
+	margin-top: 30px;
+}
+</style>

+ 33 - 0
src/views/element/tour.vue

@@ -0,0 +1,33 @@
+<template>
+    <div class="container">
+        <el-button type="primary" @click="open = true">开始引导</el-button>
+
+        <el-divider />
+
+        <el-space>
+            <el-button ref="ref1">上传</el-button>
+            <el-button ref="ref2" type="primary">保存</el-button>
+            <el-button ref="ref3" :icon="MoreFilled" />
+        </el-space>
+
+        <el-tour v-model="open">
+            <el-tour-step :target="ref1?.$el" title="上传文件">
+                <img style="width: 120px" src="../../assets/img/img.jpg" alt="tour.png" />
+                <div>点击这里选择文件</div>
+            </el-tour-step>
+            <el-tour-step :target="ref2?.$el" title="保存" description="点击进行上传" />
+            <el-tour-step :target="ref3?.$el" title="更多操作" description="点击查看更多操作" />
+        </el-tour>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { MoreFilled } from '@element-plus/icons-vue'
+
+const ref1 = ref()
+const ref2 = ref()
+const ref3 = ref()
+
+const open = ref(false)
+</script>

+ 44 - 48
src/views/upload.vue → src/views/element/upload.vue

@@ -1,48 +1,44 @@
-<template>
-    <div class="container">
-        <div class="content-title">支持拖拽</div>
-        <div class="plugins-tips">
-            Element Plus自带上传组件。 访问地址:
-            <a href="https://element-plus.org/zh-CN/component/upload.html" target="_blank">Element Plus Upload</a>
-        </div>
-        <el-upload
-            class="upload-demo"
-            drag
-            action="http://jsonplaceholder.typicode.com/api/posts/"
-            multiple
-            :on-change="handle"
-        >
-            <el-icon class="el-icon--upload"><upload-filled /></el-icon>
-            <div class="el-upload__text">
-                将文件拖到此处,或
-                <em>点击上传</em>
-            </div>
-        </el-upload>
-
-        <div class="content-title">支持裁剪</div>
-        <div class="plugins-tips">
-            vue-cropperjs:一个封装了 cropperjs 的 Vue 组件。 访问地址:
-            <a href="https://github.com/Agontuk/vue-cropperjs" target="_blank">vue-cropperjs</a>。 示例请查看
-            <router-link to="/user">个人中心</router-link>
-        </div>
-    </div>
-</template>
-
-<script setup lang="ts">
-const handle = (rawFile: any) => {
-    console.log(rawFile);
-};
-</script>
-
-<style scoped>
-.content-title {
-    font-weight: 400;
-    line-height: 50px;
-    margin: 10px 0;
-    font-size: 22px;
-    color: #1f2f3d;
-}
-.upload-demo {
-    width: 360px;
-}
-</style>
+<template>
+    <div class="container">
+        <div class="content-title">支持拖拽</div>
+        <div class="plugins-tips">
+            Element Plus自带上传组件。 访问地址:
+            <a href="https://element-plus.org/zh-CN/component/upload.html" target="_blank">Element Plus Upload</a>
+        </div>
+        <el-upload class="upload-demo" drag action="http://jsonplaceholder.typicode.com/api/posts/" multiple
+            :on-change="handle">
+            <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+            <div class="el-upload__text">
+                将文件拖到此处,或
+                <em>点击上传</em>
+            </div>
+        </el-upload>
+
+        <div class="content-title">支持裁剪</div>
+        <div class="plugins-tips">
+            vue-cropper:一个简单的vue图片裁剪插件。 访问地址:
+            <a href="https://github.com/xyxiao001/vue-cropper" target="_blank">vue-cropper</a>。 示例请查看
+            <router-link to="/ucenter">个人中心-我的头像</router-link>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+const handle = (rawFile: any) => {
+    console.log(rawFile);
+};
+</script>
+
+<style scoped>
+.content-title {
+    font-weight: 400;
+    line-height: 50px;
+    margin: 10px 0;
+    font-size: 22px;
+    color: #1f2f3d;
+}
+
+.upload-demo {
+    width: 360px;
+}
+</style>

+ 62 - 0
src/views/element/watermark.vue

@@ -0,0 +1,62 @@
+<template>
+    <div class="container">
+        <el-row :gutter="20">
+            <el-col :span="18">
+                <el-watermark :content="config.content" :font="config.font" :z-index="config.zIndex"
+                    :rotate="config.rotate" :gap="config.gap" :offset="config.offset">
+                    <div style="height: 600px" />
+                </el-watermark>
+            </el-col>
+            <el-col :span="6">
+                <el-form class="form" :model="config" label-position="top" label-width="50px">
+                    <el-form-item label="Content">
+                        <el-input v-model="config.content" />
+                    </el-form-item>
+                    <el-form-item label="Color">
+                        <el-color-picker v-model="config.font.color" show-alpha />
+                    </el-form-item>
+                    <el-form-item label="FontSize">
+                        <el-slider v-model="config.font.fontSize" />
+                    </el-form-item>
+                    <el-form-item label="zIndex">
+                        <el-slider v-model="config.zIndex" />
+                    </el-form-item>
+                    <el-form-item label="Rotate">
+                        <el-slider v-model="config.rotate" :min="-180" :max="180" />
+                    </el-form-item>
+                    <el-form-item label="Gap">
+                        <el-space>
+                            <el-input-number v-model="config.gap[0]" controls-position="right" />
+                            <el-input-number v-model="config.gap[1]" controls-position="right" />
+                        </el-space>
+                    </el-form-item>
+                    <el-form-item label="Offset">
+                        <el-space>
+                            <el-input-number v-model="config.offset[0]" placeholder="offsetLeft"
+                                controls-position="right" />
+                            <el-input-number v-model="config.offset[1]" placeholder="offsetTop"
+                                controls-position="right" />
+                        </el-space>
+                    </el-form-item>
+                </el-form>
+            </el-col>
+        </el-row>
+
+    </div>
+</template>
+
+<script setup lang="ts">
+import { reactive } from 'vue'
+
+const config = reactive({
+    content: 'vue-manage-system',
+    font: {
+        fontSize: 16,
+        color: 'rgba(0, 0, 0, 0.15)',
+    },
+    zIndex: -1,
+    rotate: -22,
+    gap: [100, 100] as [number, number],
+    offset: [] as unknown as [number, number],
+})
+</script>

+ 0 - 156
src/views/form.vue

@@ -1,156 +0,0 @@
-<template>
-    <div class="container">
-        <div class="form-box">
-            <el-form ref="formRef" :rules="rules" :model="form" label-width="80px">
-                <el-form-item label="表单名称" prop="name">
-                    <el-input v-model="form.name"></el-input>
-                </el-form-item>
-                <el-form-item label="选择器" prop="region">
-                    <el-select v-model="form.region" placeholder="请选择">
-                        <el-option key="小明" label="小明" value="小明"></el-option>
-                        <el-option key="小红" label="小红" value="小红"></el-option>
-                        <el-option key="小白" label="小白" value="小白"></el-option>
-                    </el-select>
-                </el-form-item>
-                <el-form-item label="日期时间">
-                    <el-col :span="11">
-                        <el-form-item prop="date1">
-                            <el-date-picker
-                                type="date"
-                                placeholder="选择日期"
-                                v-model="form.date1"
-                                style="width: 100%"
-                            ></el-date-picker>
-                        </el-form-item>
-                    </el-col>
-                    <el-col class="line" :span="2">-</el-col>
-                    <el-col :span="11">
-                        <el-form-item prop="date2">
-                            <el-time-picker placeholder="选择时间" v-model="form.date2" style="width: 100%">
-                            </el-time-picker>
-                        </el-form-item>
-                    </el-col>
-                </el-form-item>
-                <el-form-item label="城市级联" prop="options">
-                    <el-cascader :options="options" v-model="form.options"></el-cascader>
-                </el-form-item>
-                <el-form-item label="选择开关" prop="delivery">
-                    <el-switch v-model="form.delivery"></el-switch>
-                </el-form-item>
-                <el-form-item label="多选框" prop="type">
-                    <el-checkbox-group v-model="form.type">
-                        <el-checkbox label="小明" name="type"></el-checkbox>
-                        <el-checkbox label="小红" name="type"></el-checkbox>
-                        <el-checkbox label="小白" name="type"></el-checkbox>
-                    </el-checkbox-group>
-                </el-form-item>
-                <el-form-item label="单选框" prop="resource">
-                    <el-radio-group v-model="form.resource">
-                        <el-radio label="小明"></el-radio>
-                        <el-radio label="小红"></el-radio>
-                        <el-radio label="小白"></el-radio>
-                    </el-radio-group>
-                </el-form-item>
-                <el-form-item label="文本框" prop="desc">
-                    <el-input type="textarea" rows="5" v-model="form.desc"></el-input>
-                </el-form-item>
-                <el-form-item>
-                    <el-button type="primary" @click="onSubmit(formRef)">表单提交</el-button>
-                    <el-button @click="onReset(formRef)">重置表单</el-button>
-                </el-form-item>
-            </el-form>
-        </div>
-    </div>
-</template>
-
-<script setup lang="ts" name="baseform">
-import { reactive, ref } from 'vue';
-import { ElMessage } from 'element-plus';
-import type { FormInstance, FormRules } from 'element-plus';
-
-const options = [
-    {
-        value: 'guangdong',
-        label: '广东省',
-        children: [
-            {
-                value: 'guangzhou',
-                label: '广州市',
-                children: [
-                    {
-                        value: 'tianhe',
-                        label: '天河区',
-                    },
-                    {
-                        value: 'haizhu',
-                        label: '海珠区',
-                    },
-                ],
-            },
-            {
-                value: 'dongguan',
-                label: '东莞市',
-                children: [
-                    {
-                        value: 'changan',
-                        label: '长安镇',
-                    },
-                    {
-                        value: 'humen',
-                        label: '虎门镇',
-                    },
-                ],
-            },
-        ],
-    },
-    {
-        value: 'hunan',
-        label: '湖南省',
-        children: [
-            {
-                value: 'changsha',
-                label: '长沙市',
-                children: [
-                    {
-                        value: 'yuelu',
-                        label: '岳麓区',
-                    },
-                ],
-            },
-        ],
-    },
-];
-const rules: FormRules = {
-    name: [{ required: true, message: '请输入表单名称', trigger: 'blur' }],
-};
-const formRef = ref<FormInstance>();
-const form = reactive({
-    name: '',
-    region: '',
-    date1: '',
-    date2: '',
-    delivery: true,
-    type: ['小明'],
-    resource: '小红',
-    desc: '',
-    options: [],
-});
-// 提交
-const onSubmit = (formEl: FormInstance | undefined) => {
-    // 表单校验
-    if (!formEl) return;
-    formEl.validate((valid) => {
-        if (valid) {
-            console.log(form);
-            ElMessage.success('提交成功!');
-        } else {
-            return false;
-        }
-    });
-};
-// 重置
-const onReset = (formEl: FormInstance | undefined) => {
-    if (!formEl) return;
-    formEl.resetFields();
-};
-</script>

+ 56 - 26
src/views/home.vue

@@ -1,26 +1,56 @@
-<template>
-	<v-header />
-	<v-sidebar />
-	<div class="content-box" :class="{ 'content-collapse': sidebar.collapse }">
-		<v-tags></v-tags>
-		<div class="content">
-			<router-view v-slot="{ Component }">
-				<transition name="move" mode="out-in">
-					<keep-alive :include="tags.nameList">
-						<component :is="Component"></component>
-					</keep-alive>
-				</transition>
-			</router-view>
-		</div>
-	</div>
-</template>
-<script setup lang="ts">
-import { useSidebarStore } from '../store/sidebar';
-import { useTagsStore } from '../store/tags';
-import vHeader from '../components/header.vue';
-import vSidebar from '../components/sidebar.vue';
-import vTags from '../components/tags.vue';
-
-const sidebar = useSidebarStore();
-const tags = useTagsStore();
-</script>
+<template>
+	<v-header />
+	<v-sidebar />
+	<div class="content-box" :class="{ 'content-collapse': sidebar.collapse }">
+		<v-tabs></v-tabs>
+		<div class="content">
+			<router-view v-slot="{ Component }">
+				<transition name="move" mode="out-in">
+					<keep-alive :include="tabs.nameList">
+						<component :is="Component"></component>
+					</keep-alive>
+				</transition>
+			</router-view>
+		</div>
+	</div>
+</template>
+<script setup lang="ts">
+import { useSidebarStore } from '@/store/sidebar';
+import { useTabsStore } from '@/store/tabs';
+import vHeader from '@/components/header.vue';
+import vSidebar from '@/components/sidebar.vue';
+import vTabs from '@/components/tabs.vue';
+
+const sidebar = useSidebarStore();
+const tabs = useTabsStore();
+</script>
+
+<style>
+.content-box {
+	position: absolute;
+	left: 250px;
+	right: 0;
+	top: 70px;
+	bottom: 0;
+	padding-bottom: 30px;
+	-webkit-transition: left 0.3s ease-in-out;
+	transition: left 0.3s ease-in-out;
+	background: #eef0fc;
+}
+
+.content {
+	width: auto;
+	height: 100%;
+	padding: 20px;
+	overflow-y: scroll;
+	box-sizing: border-box;
+}
+
+.content::-webkit-scrollbar {
+	width: 0;
+}
+
+.content-collapse {
+	left: 65px;
+}
+</style>

+ 67 - 0
src/views/pages/403.vue

@@ -0,0 +1,67 @@
+<template>
+	<div class="error-page">
+		<div class="error-box">
+			<div class="error-code">403</div>
+			<div class="error-desc">啊哦~ 你没有权限访问该页面哦</div>
+			<div class="error-handle">
+				<router-link to="/">
+					<el-button type="primary" size="large">返回首页</el-button>
+				</router-link>
+				<el-button class="error-btn" size="large" @click="goBack">返回上一页</el-button>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="403">
+import { useRouter } from 'vue-router';
+
+const router = useRouter();
+const goBack = () => {
+	router.go(-2);
+};
+</script>
+
+<style scoped>
+.error-page {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	flex-direction: column;
+	width: 100%;
+	height: 100%;
+	background: #eef0fc;
+	box-sizing: border-box;
+}
+
+.error-box {
+	width: 400px;
+	background-color: #fff;
+	padding: 80px 50px;
+	border-radius: 5px;
+}
+
+.error-code {
+	line-height: 1;
+	font-size: 100px;
+	font-weight: bold;
+	color: var(--el-color-primary);
+	margin-bottom: 20px;
+	text-align: center
+}
+
+.error-desc {
+	font-size: 20px;
+	color: #777;
+	text-align: center
+}
+
+.error-handle {
+	margin-top: 50px;
+	text-align: center;
+}
+
+.error-btn {
+	margin-left: 100px;
+}
+</style>

+ 67 - 0
src/views/pages/404.vue

@@ -0,0 +1,67 @@
+<template>
+	<div class="error-page">
+		<div class="error-box">
+			<div class="error-code">404</div>
+			<div class="error-desc">啊哦~ 你所访问的页面不存在</div>
+			<div class="error-handle">
+				<router-link to="/">
+					<el-button type="primary" size="large">返回首页</el-button>
+				</router-link>
+				<el-button class="error-btn" size="large" @click="goBack">返回上一页</el-button>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="404">
+import { useRouter } from 'vue-router';
+
+const router = useRouter();
+const goBack = () => {
+	router.go(-1);
+};
+</script>
+
+<style scoped>
+.error-page {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	flex-direction: column;
+	width: 100%;
+	height: 100%;
+	background: #eef0fc;
+	box-sizing: border-box;
+}
+
+.error-box {
+	width: 400px;
+	background-color: #fff;
+	padding: 80px 50px;
+	border-radius: 5px;
+}
+
+.error-code {
+	line-height: 1;
+	font-size: 100px;
+	font-weight: bold;
+	color: var(--el-color-primary);
+	margin-bottom: 20px;
+	text-align: center
+}
+
+.error-desc {
+	font-size: 20px;
+	color: #777;
+	text-align: center
+}
+
+.error-handle {
+	margin-top: 50px;
+	text-align: center;
+}
+
+.error-btn {
+	margin-left: 100px;
+}
+</style>

+ 0 - 0
src/views/editor.vue → src/views/pages/editor.vue


+ 257 - 212
src/views/icon.vue → src/views/pages/icon.vue

@@ -1,212 +1,257 @@
-<template>
-	<div class="container">
-		<h2>使用方法</h2>
-		<p style="line-height: 50px">
-			直接通过设置类名为 el-icon-lx-iconName 来使用即可。例如:(共{{ iconList.length }}个图标)
-		</p>
-		<p class="example-p">
-			<i class="el-icon-lx-redpacket_fill" style="font-size: 30px; color: #ff5900"></i>
-			<span>&lt;i class=&quot;el-icon-lx-redpacket_fill&quot;&gt;&lt;/i&gt;</span>
-		</p>
-		<p class="example-p">
-			<i class="el-icon-lx-weibo" style="font-size: 30px; color: #fd5656"></i>
-			<span>&lt;i class=&quot;el-icon-lx-weibo&quot;&gt;&lt;/i&gt;</span>
-		</p>
-		<p class="example-p">
-			<i class="el-icon-lx-emojifill" style="font-size: 30px; color: #ffc300"></i>
-			<span>&lt;i class=&quot;el-icon-lx-emojifill&quot;&gt;&lt;/i&gt;</span>
-		</p>
-		<br />
-		<h2>图标</h2>
-		<div class="search-box">
-			<el-input class="search" size="large" v-model="keyword" clearable placeholder="请输入图标名称"></el-input>
-		</div>
-		<ul>
-			<li class="icon-li" v-for="(item, index) in list" :key="index">
-				<div class="icon-li-content">
-					<i :class="`el-icon-lx-${item}`"></i>
-					<span>{{ item }}</span>
-				</div>
-			</li>
-		</ul>
-	</div>
-</template>
-
-<script setup lang="ts" name="icon">
-import { computed, ref } from 'vue';
-
-const iconList: Array<string> = [
-	'attentionforbid',
-	'attentionforbidfill',
-	'attention',
-	'attentionfill',
-	'tag',
-	'tagfill',
-	'people',
-	'peoplefill',
-	'notice',
-	'noticefill',
-	'mobile',
-	'mobilefill',
-	'voice',
-	'voicefill',
-	'unlock',
-	'lock',
-	'home',
-	'homefill',
-	'delete',
-	'deletefill',
-	'notification',
-	'notificationfill',
-	'notificationforbidfill',
-	'like',
-	'likefill',
-	'comment',
-	'commentfill',
-	'camera',
-	'camerafill',
-	'warn',
-	'warnfill',
-	'time',
-	'timefill',
-	'location',
-	'locationfill',
-	'favor',
-	'favorfill',
-	'skin',
-	'skinfill',
-	'news',
-	'newsfill',
-	'record',
-	'recordfill',
-	'emoji',
-	'emojifill',
-	'message',
-	'messagefill',
-	'goods',
-	'goodsfill',
-	'crown',
-	'crownfill',
-	'move',
-	'add',
-	'hot',
-	'hotfill',
-	'service',
-	'servicefill',
-	'present',
-	'presentfill',
-	'pic',
-	'picfill',
-	'rank',
-	'rankfill',
-	'male',
-	'female',
-	'down',
-	'top',
-	'recharge',
-	'rechargefill',
-	'forward',
-	'forwardfill',
-	'info',
-	'infofill',
-	'redpacket',
-	'redpacket_fill',
-	'roundadd',
-	'roundaddfill',
-	'friendadd',
-	'friendaddfill',
-	'cart',
-	'cartfill',
-	'more',
-	'moreandroid',
-	'back',
-	'right',
-	'shop',
-	'shopfill',
-	'question',
-	'questionfill',
-	'roundclose',
-	'roundclosefill',
-	'roundcheck',
-	'roundcheckfill',
-	'global',
-	'mail',
-	'punch',
-	'exit',
-	'upload',
-	'read',
-	'file',
-	'link',
-	'full',
-	'group',
-	'friend',
-	'profile',
-	'addressbook',
-	'calendar',
-	'text',
-	'copy',
-	'share',
-	'wifi',
-	'vipcard',
-	'weibo',
-	'remind',
-	'refresh',
-	'filter',
-	'settings',
-	'scan',
-	'qrcode',
-	'cascades',
-	'apps',
-	'sort',
-	'searchlist',
-	'search',
-	'edit'
-];
-const keyword = ref('');
-const list = computed(() => {
-	return iconList.filter(item => {
-		return item.indexOf(keyword.value) !== -1;
-	});
-});
-</script>
-
-<style scoped>
-.example-p {
-	height: 45px;
-	display: flex;
-	align-items: center;
-}
-.search-box {
-	text-align: center;
-	margin-top: 10px;
-}
-.search {
-	width: 300px;
-}
-ul,
-li {
-	list-style: none;
-}
-.icon-li {
-	display: inline-block;
-	padding: 10px;
-	width: 120px;
-	height: 120px;
-}
-.icon-li-content {
-	display: flex;
-	height: 100%;
-	flex-direction: column;
-	align-items: center;
-	justify-content: center;
-	cursor: pointer;
-}
-.icon-li-content i {
-	font-size: 36px;
-	color: #606266;
-}
-.icon-li-content span {
-	margin-top: 10px;
-	color: #787878;
-}
-</style>
+<template>
+
+	<el-tabs type="border-card">
+		<el-tab-pane label="自定义图标">
+			<h2>使用方法</h2>
+			<p style="line-height: 50px">
+				直接通过设置类名为 el-icon-lx-iconName 来使用即可。例如:(共{{ iconList.length }}个图标)
+			</p>
+			<p class="example-p">
+				<i class="el-icon-lx-redpacket_fill" style="font-size: 30px; color: #ff5900"></i>
+				<span>&lt;i class=&quot;el-icon-lx-redpacket_fill&quot;&gt;&lt;/i&gt;</span>
+			</p>
+			<p class="example-p">
+				<i class="el-icon-lx-weibo" style="font-size: 30px; color: #fd5656"></i>
+				<span>&lt;i class=&quot;el-icon-lx-weibo&quot;&gt;&lt;/i&gt;</span>
+			</p>
+			<p class="example-p">
+				<i class="el-icon-lx-emojifill" style="font-size: 30px; color: #ffc300"></i>
+				<span>&lt;i class=&quot;el-icon-lx-emojifill&quot;&gt;&lt;/i&gt;</span>
+			</p>
+			<br />
+			<h2>图标</h2>
+			<div class="search-box">
+				<el-input class="search" size="large" v-model="keyword" clearable placeholder="请输入图标名称"></el-input>
+			</div>
+			<ul>
+				<li class="icon-li" v-for="(item, index) in list" :key="index">
+					<div class="icon-li-content">
+						<i :class="`el-icon-lx-${item}`"></i>
+						<span>{{ item }}</span>
+					</div>
+				</li>
+			</ul>
+		</el-tab-pane>
+		<el-tab-pane label="Element图标">
+			<el-link type="primary" href="https://element-plus.org/zh-CN/component/icon.html#icon-collection"
+				target="_blank">前往官方文档查看</el-link>
+		</el-tab-pane>
+	</el-tabs>
+</template>
+
+<script setup lang="ts" name="icon">
+import { computed, ref } from 'vue';
+
+const iconList: Array<string> = [
+	'attentionforbid',
+	'attentionforbidfill',
+	'attention',
+	'attentionfill',
+	'tag',
+	'tagfill',
+	'people',
+	'peoplefill',
+	'notice',
+	'noticefill',
+	'mobile',
+	'mobilefill',
+	'voice',
+	'voicefill',
+	'unlock',
+	'lock',
+	'home',
+	'homefill',
+	'delete',
+	'deletefill',
+	'notification',
+	'notificationfill',
+	'notificationforbidfill',
+	'like',
+	'likefill',
+	'comment',
+	'commentfill',
+	'camera',
+	'camerafill',
+	'warn',
+	'warnfill',
+	'time',
+	'timefill',
+	'location',
+	'locationfill',
+	'favor',
+	'favorfill',
+	'skin',
+	'skinfill',
+	'news',
+	'newsfill',
+	'record',
+	'recordfill',
+	'emoji',
+	'emojifill',
+	'message',
+	'messagefill',
+	'goods',
+	'goodsfill',
+	'crown',
+	'crownfill',
+	'move',
+	'add',
+	'hot',
+	'hotfill',
+	'service',
+	'servicefill',
+	'present',
+	'presentfill',
+	'pic',
+	'picfill',
+	'rank',
+	'rankfill',
+	'male',
+	'female',
+	'down',
+	'top',
+	'recharge',
+	'rechargefill',
+	'forward',
+	'forwardfill',
+	'info',
+	'infofill',
+	'redpacket',
+	'redpacket_fill',
+	'roundadd',
+	'roundaddfill',
+	'friendadd',
+	'friendaddfill',
+	'cart',
+	'cartfill',
+	'more',
+	'moreandroid',
+	'back',
+	'right',
+	'shop',
+	'shopfill',
+	'question',
+	'questionfill',
+	'roundclose',
+	'roundclosefill',
+	'roundcheck',
+	'roundcheckfill',
+	'global',
+	'mail',
+	'punch',
+	'exit',
+	'upload',
+	'read',
+	'file',
+	'link',
+	'full',
+	'group',
+	'friend',
+	'profile',
+	'addressbook',
+	'calendar',
+	'text',
+	'copy',
+	'share',
+	'wifi',
+	'vipcard',
+	'weibo',
+	'remind',
+	'refresh',
+	'filter',
+	'settings',
+	'scan',
+	'qrcode',
+	'cascades',
+	'apps',
+	'sort',
+	'searchlist',
+	'search',
+	'edit',
+	'apple-line',
+	'baidu-fill',
+	'amazon-fill',
+	'netease-cloud-music-fill',
+	'qq-line',
+	'wechat-fill',
+	'alipay-fill',
+	'android-fill',
+	'android-line',
+	'whatsapp-line',
+	'whatsapp-fill',
+	'bilibili-fill',
+	'chrome-fill',
+	'dingding-fill',
+	'dingding-line',
+	'apple-fill',
+	'github-fill',
+	'qq-fill',
+	'wechat-pay-fill',
+	'windows-line',
+	'windows-fill',
+	'youtube-line',
+	'youtube-fill',
+	'wechat-pay-line',
+	'zhihu-line'
+];
+
+const keyword = ref('');
+const list = computed(() => {
+	return iconList.filter(item => {
+		return item.indexOf(keyword.value) !== -1;
+	});
+});
+</script>
+
+<style scoped>
+.example-p {
+	height: 45px;
+	display: flex;
+	align-items: center;
+}
+
+.search-box {
+	text-align: center;
+	margin-top: 10px;
+}
+
+.search {
+	width: 300px;
+}
+
+ul,
+li {
+	list-style: none;
+}
+
+.icon-li {
+	display: inline-block;
+	padding: 10px;
+	width: 120px;
+	height: 120px;
+}
+
+.icon-li-content {
+	display: flex;
+	height: 100%;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	cursor: pointer;
+}
+
+.icon-li-content i {
+	font-size: 36px;
+	color: #606266;
+}
+
+.icon-li-content span {
+	margin-top: 10px;
+	color: #787878;
+}
+
+.iframe {
+	width: 100%;
+	height: 700px;
+}
+</style>

+ 172 - 136
src/views/login.vue → src/views/pages/login.vue

@@ -1,136 +1,172 @@
-<template>
-    <div class="login-wrap">
-        <div class="ms-login">
-            <div class="ms-title">后台管理系统</div>
-            <el-form :model="param" :rules="rules" ref="login" label-width="0px" class="ms-content">
-                <el-form-item prop="username">
-                    <el-input v-model="param.username" placeholder="username">
-                        <template #prepend>
-                            <el-button :icon="User"></el-button>
-                        </template>
-                    </el-input>
-                </el-form-item>
-                <el-form-item prop="password">
-                    <el-input
-                        type="password"
-                        placeholder="password"
-                        v-model="param.password"
-                        @keyup.enter="submitForm(login)"
-                    >
-                        <template #prepend>
-                            <el-button :icon="Lock"></el-button>
-                        </template>
-                    </el-input>
-                </el-form-item>
-                <div class="login-btn">
-                    <el-button type="primary" @click="submitForm(login)">登录</el-button>
-                </div>
-                <el-checkbox class="login-tips" v-model="checked" label="记住密码" size="large" />
-                <p class="login-tips">Tips : 用户名和密码随便填。</p>
-            </el-form>
-        </div>
-    </div>
-</template>
-
-<script setup lang="ts">
-import { ref, reactive } from 'vue';
-import { useTagsStore } from '../store/tags';
-import { usePermissStore } from '../store/permiss';
-import { useRouter } from 'vue-router';
-import { ElMessage } from 'element-plus';
-import type { FormInstance, FormRules } from 'element-plus';
-import { Lock, User } from '@element-plus/icons-vue';
-
-interface LoginInfo {
-    username: string;
-    password: string;
-}
-
-const lgStr = localStorage.getItem('login-param');
-const defParam = lgStr ? JSON.parse(lgStr) : null;
-const checked = ref(lgStr ? true : false);
-
-const router = useRouter();
-const param = reactive<LoginInfo>({
-    username: defParam ? defParam.username : '',
-    password: defParam ? defParam.password : '',
-});
-
-const rules: FormRules = {
-    username: [
-        {
-            required: true,
-            message: '请输入用户名',
-            trigger: 'blur',
-        },
-    ],
-    password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
-};
-const permiss = usePermissStore();
-const login = ref<FormInstance>();
-const submitForm = (formEl: FormInstance | undefined) => {
-    if (!formEl) return;
-    formEl.validate((valid: boolean) => {
-        if (valid) {
-            ElMessage.success('登录成功');
-            localStorage.setItem('ms_username', param.username);
-            const keys = permiss.defaultList[param.username == 'admin' ? 'admin' : 'user'];
-            permiss.handleSet(keys);
-            localStorage.setItem('ms_keys', JSON.stringify(keys));
-            router.push('/');
-            if (checked.value) {
-                localStorage.setItem('login-param', JSON.stringify(param));
-            } else {
-                localStorage.removeItem('login-param');
-            }
-        } else {
-            ElMessage.error('登录失败');
-            return false;
-        }
-    });
-};
-
-const tags = useTagsStore();
-tags.clearTags();
-</script>
-
-<style scoped>
-.login-wrap {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    width: 100%;
-    height: 100%;
-    background-image: url(../assets/img/login-bg.jpg);
-    background-size: 100%;
-}
-.ms-title {
-    line-height: 50px;
-    text-align: center;
-    font-size: 20px;
-    color: #333;
-    font-weight: bold;
-    padding-top: 10px;
-}
-.ms-login {
-    width: 350px;
-    border-radius: 5px;
-    background: #fff;
-}
-.ms-content {
-    padding: 10px 30px 30px;
-}
-.login-btn {
-    text-align: center;
-}
-.login-btn button {
-    width: 100%;
-    height: 36px;
-    margin-bottom: 10px;
-}
-.login-tips {
-    font-size: 12px;
-    line-height: 30px;
-    color: #333;
-}
-</style>
+<template>
+    <div class="login-bg">
+        <div class="login-container">
+            <div class="login-header">
+                <img class="logo mr10" src="../../assets/img/logo.svg" alt="" />
+                <div class="login-title">后台管理系统</div>
+            </div>
+            <el-form :model="param" :rules="rules" ref="login" size="large">
+                <el-form-item prop="username">
+                    <el-input v-model="param.username" placeholder="用户名">
+                        <template #prepend>
+                            <el-icon>
+                                <User />
+                            </el-icon>
+                        </template>
+                    </el-input>
+                </el-form-item>
+                <el-form-item prop="password">
+                    <el-input
+                        type="password"
+                        placeholder="密码"
+                        v-model="param.password"
+                        @keyup.enter="submitForm(login)"
+                    >
+                        <template #prepend>
+                            <el-icon>
+                                <Lock />
+                            </el-icon>
+                        </template>
+                    </el-input>
+                </el-form-item>
+                <div class="pwd-tips">
+                    <el-checkbox class="pwd-checkbox" v-model="checked" label="记住密码" />
+                    <el-link type="primary" @click="$router.push('/reset-pwd')">忘记密码</el-link>
+                </div>
+                <el-button class="login-btn" type="primary" size="large" @click="submitForm(login)">登录</el-button>
+                <p class="login-tips">Tips : 用户名和密码随便填。</p>
+                <p class="login-text">
+                    没有账号?<el-link type="primary" @click="$router.push('/register')">立即注册</el-link>
+                </p>
+            </el-form>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue';
+import { useTabsStore } from '@/store/tabs';
+import { usePermissStore } from '@/store/permiss';
+import { useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
+
+interface LoginInfo {
+    username: string;
+    password: string;
+}
+
+const lgStr = localStorage.getItem('login-param');
+const defParam = lgStr ? JSON.parse(lgStr) : null;
+const checked = ref(lgStr ? true : false);
+
+const router = useRouter();
+const param = reactive<LoginInfo>({
+    username: defParam ? defParam.username : '',
+    password: defParam ? defParam.password : '',
+});
+
+const rules: FormRules = {
+    username: [
+        {
+            required: true,
+            message: '请输入用户名',
+            trigger: 'blur',
+        },
+    ],
+    password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+};
+const permiss = usePermissStore();
+const login = ref<FormInstance>();
+const submitForm = (formEl: FormInstance | undefined) => {
+    if (!formEl) return;
+    formEl.validate((valid: boolean) => {
+        if (valid) {
+            ElMessage.success('登录成功');
+            localStorage.setItem('ms_username', param.username);
+            const keys = permiss.defaultList[param.username == 'admin' ? 'admin' : 'user'];
+            permiss.handleSet(keys);
+            localStorage.setItem('ms_keys', JSON.stringify(keys));
+            router.push('/');
+            if (checked.value) {
+                localStorage.setItem('login-param', JSON.stringify(param));
+            } else {
+                localStorage.removeItem('login-param');
+            }
+        } else {
+            ElMessage.error('登录失败');
+            return false;
+        }
+    });
+};
+
+const tabs = useTabsStore();
+tabs.clearTabs();
+</script>
+
+<style scoped>
+.login-bg {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    background: url(../../assets/img/login-bg.jpg) center/cover no-repeat;
+}
+
+.login-header {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 40px;
+}
+
+.logo {
+    width: 35px;
+}
+
+.login-title {
+    font-size: 22px;
+    color: #333;
+    font-weight: bold;
+}
+
+.login-container {
+    width: 450px;
+    border-radius: 5px;
+    background: #fff;
+    padding: 40px 50px 50px;
+    box-sizing: border-box;
+}
+
+.pwd-tips {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: 14px;
+    margin: -10px 0 10px;
+    color: #787878;
+}
+
+.pwd-checkbox {
+    height: auto;
+}
+
+.login-btn {
+    display: block;
+    width: 100%;
+}
+
+.login-tips {
+    font-size: 12px;
+    color: #999;
+}
+
+.login-text {
+    display: flex;
+    align-items: center;
+    margin-top: 20px;
+    font-size: 14px;
+    color: #787878;
+}
+</style>

+ 21 - 21
src/views/markdown.vue → src/views/pages/markdown.vue

@@ -1,21 +1,21 @@
-<template>
-	<div class="container">
-		<div class="plugins-tips">
-			md-editor-v3:vue3版本的 markdown 编辑器,配置丰富,请详看文档。 访问地址:
-			<a href="https://imzbf.github.io/md-editor-v3/index" target="_blank">md-editor-v3</a>
-		</div>
-		<md-editor class="mgb20" v-model="text" @on-upload-img="onUploadImg" />
-		<el-button type="primary">提交</el-button>
-	</div>
-</template>
-
-<script setup lang="ts" name="md">
-import { ref } from 'vue';
-import MdEditor from 'md-editor-v3';
-import 'md-editor-v3/lib/style.css';
-
-const text = ref('Hello Editor!');
-const onUploadImg = (files: any) => {
-	console.log(files);
-};
-</script>
+<template>
+	<div class="container">
+		<div class="plugins-tips">
+			md-editor-v3:vue3版本的 markdown 编辑器,配置丰富,请详看文档。 访问地址:
+			<a href="https://imzbf.github.io/md-editor-v3/index" target="_blank">md-editor-v3</a>
+		</div>
+		<md-editor class="mgb20" v-model="text" @on-upload-img="onUploadImg" />
+		<el-button type="primary">提交</el-button>
+	</div>
+</template>
+
+<script setup lang="ts" name="md">
+import { ref } from 'vue';
+import MdEditor from 'md-editor-v3';
+import 'md-editor-v3/lib/style.css';
+
+const text = ref('Hello Editor!');
+const onUploadImg = (files: any) => {
+	console.log(files);
+};
+</script>

+ 132 - 0
src/views/pages/register.vue

@@ -0,0 +1,132 @@
+<template>
+    <div class="login-bg">
+        <div class="login-container">
+            <div class="login-header">
+                <img class="logo mr10" src="../../assets/img/logo.svg" alt="">
+                <div class="login-title">
+                    后台管理系统
+                </div>
+            </div>
+            <el-form :model="param" :rules="rules" ref="register" size="large">
+                <el-form-item prop="username">
+                    <el-input v-model="param.username" placeholder="用户名">
+                        <template #prepend>
+                            <el-icon>
+                                <User />
+                            </el-icon>
+                        </template>
+                    </el-input>
+                </el-form-item>
+                <el-form-item prop="email">
+                    <el-input v-model="param.email" placeholder="邮箱">
+                        <template #prepend>
+                            <el-icon>
+                                <Message />
+                            </el-icon>
+                        </template>
+                    </el-input>
+                </el-form-item>
+                <el-form-item prop="password">
+                    <el-input type="password" placeholder="密码" v-model="param.password"
+                        @keyup.enter="submitForm(register)">
+                        <template #prepend>
+                            <el-icon>
+                                <Lock />
+                            </el-icon>
+                        </template>
+                    </el-input>
+                </el-form-item>
+                <el-button class="login-btn" type="primary" size="large" @click="submitForm(register)">注册</el-button>
+                <p class="login-text">已有账号,<el-link type="primary" @click="$router.push('/login')">立即登录</el-link></p>
+            </el-form>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue';
+import { useRouter } from 'vue-router';
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
+import { Register } from '@/types/user';
+
+const router = useRouter();
+const param = reactive<Register>({
+    username: '',
+    password: '',
+    email: '',
+});
+
+const rules: FormRules = {
+    username: [
+        {
+            required: true,
+            message: '请输入用户名',
+            trigger: 'blur',
+        },
+    ],
+    password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+    email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }],
+};
+const register = ref<FormInstance>();
+const submitForm = (formEl: FormInstance | undefined) => {
+    if (!formEl) return;
+    formEl.validate((valid: boolean) => {
+        if (valid) {
+            ElMessage.success('注册成功,请登录');
+            router.push('/login');
+        } else {
+            return false;
+        }
+    });
+};
+
+</script>
+
+<style scoped>
+.login-bg {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    background: url(../../assets/img/login-bg.jpg) center/cover no-repeat;
+}
+
+.login-header {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 40px;
+}
+
+.logo {
+    width: 35px;
+}
+
+.login-title {
+    font-size: 22px;
+    color: #333;
+    font-weight: bold;
+}
+
+.login-container {
+    width: 450px;
+    border-radius: 5px;
+    background: #fff;
+    padding: 40px 50px 50px;
+    box-sizing: border-box;
+}
+
+.login-btn {
+    display: block;
+    width: 100%;
+}
+
+.login-text {
+    display: flex;
+    align-items: center;
+    margin-top: 20px;
+    font-size: 14px;
+    color: #787878;
+}
+</style>

+ 97 - 0
src/views/pages/reset-pwd.vue

@@ -0,0 +1,97 @@
+<template>
+    <div class="login-bg">
+        <div class="login-container">
+            <div class="reset-title">重置密码</div>
+            <p class="reset-text">输入你的邮箱,我们将发送重置密码邮件</p>
+            <el-form :model="param" :rules="rules" ref="register" size="large">
+                <el-form-item prop="email">
+                    <el-input v-model="param.email" placeholder="邮箱">
+                        <template #prepend>
+                            <el-icon>
+                                <Message />
+                            </el-icon>
+                        </template>
+                    </el-input>
+                </el-form-item>
+                <el-button class="login-btn" type="primary" size="large" @click="submitForm(register)">发送邮件</el-button>
+                <p class="login-text"><el-link type="primary" @click="$router.push('/login')">返回登录</el-link></p>
+            </el-form>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
+
+const param = ref({
+    email: '',
+});
+
+const rules: FormRules = {
+    email: [
+        { required: true, message: '请输入邮箱', trigger: 'blur' },
+        { pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, message: '请输入正确的邮箱格式', trigger: 'blur' }
+    ],
+};
+const register = ref<FormInstance>();
+const submitForm = (formEl: FormInstance | undefined) => {
+    if (!formEl) return;
+    formEl.validate((valid: boolean) => {
+        if (valid) {
+            ElMessage.success('邮件已发送,请注意查收');
+        } else {
+            return false;
+        }
+    });
+};
+
+</script>
+
+<style scoped>
+.login-bg {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    background: url(../../assets/img/login-bg.jpg) center/cover no-repeat;
+}
+
+.reset-title {
+    text-align: center;
+    font-size: 22px;
+    color: #333;
+    font-weight: bold;
+    margin-bottom: 10px;
+}
+
+.reset-text {
+    text-align: center;
+    font-size: 14px;
+    color: #787878;
+    margin-bottom: 40px;
+}
+
+.login-container {
+    width: 450px;
+    border-radius: 5px;
+    background: #fff;
+    padding: 40px 50px 50px;
+    box-sizing: border-box;
+}
+
+.login-btn {
+    display: block;
+    width: 100%;
+}
+
+.login-text {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: 20px;
+    font-size: 14px;
+    color: #333;
+}
+</style>

+ 205 - 0
src/views/pages/theme.vue

@@ -0,0 +1,205 @@
+<template>
+    <div>
+        <el-card class="mgb20" shadow="hover">
+            <template #header>
+                <div class="content-title">系统主题</div>
+            </template>
+            <div class="theme-list mgb20">
+                <div class="theme-item" @click="setSystemTheme(item)" v-for="item in system"
+                    :style="{ backgroundColor: item.color, color: '#fff' }">{{ item.name }}
+                </div>
+            </div>
+            <div class="flex-center">
+                <el-button @click="resetSystemTheme">重置主题</el-button>
+            </div>
+        </el-card>
+        <el-card class="mgb20" shadow="hover">
+            <template #header>
+                <div class="content-title">Element-Plus主题</div>
+            </template>
+            <div class="theme-list mgb20">
+                <div class="theme-item" v-for="theme in themes">
+                    <el-button :type="theme.name">{{ theme.name }}</el-button>
+                    <div class="theme-color">{{ theme.color }}</div>
+                    <el-color-picker v-model="color[theme.name]" @change="changeColor(theme.name)" />
+                </div>
+            </div>
+            <div class="flex-center">
+                <el-button @click="resetTheme">重置主题</el-button>
+            </div>
+        </el-card>
+
+        <el-row :gutter="50">
+            <el-col :span="12">
+                <el-card class="mgb20" shadow="hover">
+                    <template #header>
+                        <div class="content-title">头部主题</div>
+                    </template>
+                    <div class="theme-list mgb20">
+                        <div class="theme-item">
+                            <el-button :color="color.headerBgColor">背景颜色</el-button>
+                            <div class="theme-color">{{ color.headerBgColor }}</div>
+                            <el-color-picker v-model="color.headerBgColor"
+                                @change="themeStore.setHeaderBgColor(color.headerBgColor)" />
+                        </div>
+                        <div class="theme-item">
+                            <el-button :color="color.headerTextColor">文字颜色</el-button>
+                            <div class="theme-color">{{ color.headerTextColor }}</div>
+                            <el-color-picker v-model="color.headerTextColor"
+                                @change="themeStore.setHeaderTextColor(color.headerTextColor)" />
+                        </div>
+                    </div>
+                    <div class="flex-center">
+                        <el-button @click="resetHeader">重置主题</el-button>
+                    </div>
+                </el-card>
+            </el-col>
+
+            <el-col :span="12">
+                <el-card class="mgb20" shadow="hover">
+                    <template #header>
+                        <div class="content-title">菜单主题</div>
+                    </template>
+                    <div class="theme-list mgb20">
+                        <div class="theme-item">
+                            <el-button :color="sidebar.bgColor">背景颜色</el-button>
+                            <div class="theme-color">{{ sidebar.bgColor }}</div>
+                            <el-color-picker v-model="sidebarColor.bgColor"
+                                @change="sidebar.setBgColor(sidebarColor.bgColor)" />
+                        </div>
+                        <div class="theme-item">
+                            <el-button :color="sidebar.textColor">文字颜色</el-button>
+                            <div class="theme-color">{{ sidebar.textColor }}</div>
+                            <el-color-picker v-model="sidebarColor.textColor"
+                                @change="sidebar.setTextColor(sidebarColor.textColor)" />
+                        </div>
+                    </div>
+                    <div class="flex-center">
+                        <el-button @click="resetSidebar">重置主题</el-button>
+                    </div>
+                </el-card>
+            </el-col>
+        </el-row>
+
+    </div>
+</template>
+
+<script setup lang="ts">
+import { useSidebarStore } from '@/store/sidebar';
+import { useThemeStore } from '@/store/theme'
+import { reactive } from 'vue';
+const themeStore = useThemeStore();
+const sidebar = useSidebarStore();
+
+const color = reactive({
+    primary: localStorage.getItem('theme-primary') || '#409eff',
+    success: localStorage.getItem('theme-success') || '#67c23a',
+    warning: localStorage.getItem('theme-warning') || '#e6a23c',
+    danger: localStorage.getItem('theme-danger') || '#f56c6c',
+    info: localStorage.getItem('theme-info') || '#909399',
+    headerBgColor: themeStore.headerBgColor,
+    headerTextColor: themeStore.headerTextColor,
+})
+const sidebarColor = reactive({
+    bgColor: sidebar.bgColor,
+    textColor: sidebar.textColor
+})
+const themes = [
+    {
+        name: 'primary',
+        color: themeStore.primary || color.primary
+    },
+    {
+        name: 'success',
+        color: themeStore.success || color.success
+    },
+    {
+        name: 'warning',
+        color: themeStore.warning || color.warning
+    },
+    {
+        name: 'danger',
+        color: themeStore.danger || color.danger
+    },
+    {
+        name: 'info',
+        color: themeStore.info || color.info
+    }
+]
+
+const changeColor = (name: string) => {
+    themeStore.setPropertyColor(color[name], name)
+}
+
+const resetTheme = () => {
+    themeStore.resetTheme()
+}
+const resetHeader = () => {
+    localStorage.removeItem('header-bg-color')
+    localStorage.removeItem('header-text-color')
+    location.reload()
+}
+const resetSidebar = () => {
+    localStorage.removeItem('sidebar-bg-color')
+    localStorage.removeItem('sidebar-text-color')
+    location.reload()
+}
+const system = [
+    {
+        name: '默认',
+        color: '#242f42'
+    },
+    {
+        name: '健康',
+        color: '#1ABC9C'
+    },
+    {
+        name: '优雅',
+        color: '#722ed1'
+    },
+    {
+        name: '热情',
+        color: '#f44336'
+    },
+    {
+        name: '宁静',
+        color: '#00bcd4'
+    }
+]
+const setSystemTheme = (data: any) => {
+    if (data.name === '默认') {
+        resetSystemTheme()
+    } else {
+        themeStore.setHeaderBgColor(data.color)
+        themeStore.setHeaderTextColor('#fff')
+        sidebar.setBgColor('#fff')
+        sidebar.setTextColor('#5b6e88')
+        themeStore.setPropertyColor(data.color, 'primary')
+    }
+}
+const resetSystemTheme = () => {
+    resetTheme();
+    resetHeader();
+    resetSidebar();
+}
+</script>
+
+<style scoped>
+.theme-list {
+    display: flex;
+    justify-content: center;
+}
+
+.theme-item {
+    margin-right: 20px;
+    padding: 30px;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    text-align: center;
+}
+
+.theme-color {
+    color: #787878;
+    margin: 20px 0;
+}
+</style>

+ 270 - 0
src/views/pages/ucenter.vue

@@ -0,0 +1,270 @@
+<template>
+    <div>
+        <div class="user-container">
+            <el-card class="user-profile" shadow="hover" :body-style="{ padding: '0px' }">
+                <div class="user-profile-bg"></div>
+                <div class="user-avatar-wrap">
+                    <el-avatar class="user-avatar" :size="120" :src="avatarImg" />
+                </div>
+                <div class="user-info">
+                    <div class="info-name">{{ name }}</div>
+                    <div class="info-desc">
+                        <span>@lin-xin</span>
+                        <el-divider direction="vertical" />
+                        <el-link href="https://lin-xin.gitee.io" target="_blank">lin-xin.gitee.io</el-link>
+                    </div>
+                    <div class="info-desc">FE Developer</div>
+                    <div class="info-icon">
+                        <a href="https://github.com/lin-xin" target="_blank"> <i class="el-icon-lx-github-fill"></i></a>
+                        <i class="el-icon-lx-qq-fill"></i>
+                        <i class="el-icon-lx-facebook-fill"></i>
+                        <i class="el-icon-lx-twitter-fill"></i>
+                    </div>
+                </div>
+                <div class="user-footer">
+                    <div class="user-footer-item">
+                        <el-statistic title="Follower" :value="1800" />
+                    </div>
+                    <div class="user-footer-item">
+                        <el-statistic title="Following" :value="666" />
+                    </div>
+                    <div class="user-footer-item">
+                        <el-statistic title="Total Post" :value="888" />
+                    </div>
+                </div>
+            </el-card>
+            <el-card
+                class="user-content"
+                shadow="hover"
+                :body-style="{ padding: '20px 50px', height: '100%', boxSizing: 'border-box' }"
+            >
+                <el-tabs tab-position="left" v-model="activeName">
+                    <el-tab-pane name="label1" label="消息通知" class="user-tabpane">
+                        <TabsComp />
+                    </el-tab-pane>
+                    <el-tab-pane name="label2" label="我的头像" class="user-tabpane">
+                        <div class="crop-wrap" v-if="activeName === 'label2'">
+                            <vueCropper
+                                ref="cropper"
+                                :img="imgSrc"
+                                :autoCrop="true"
+                                :centerBox="true"
+                                :full="true"
+                                mode="contain"
+                            >
+                            </vueCropper>
+                        </div>
+                        <el-button class="crop-demo-btn" type="primary"
+                            >选择图片
+                            <input class="crop-input" type="file" name="image" accept="image/*" @change="setImage" />
+                        </el-button>
+                        <el-button type="success" @click="saveAvatar">上传并保存</el-button>
+                    </el-tab-pane>
+                    <el-tab-pane name="label3" label="修改密码" class="user-tabpane">
+                        <el-form class="w500" label-position="top">
+                            <el-form-item label="旧密码:">
+                                <el-input type="password" v-model="form.old"></el-input>
+                            </el-form-item>
+                            <el-form-item label="新密码:">
+                                <el-input type="password" v-model="form.new"></el-input>
+                            </el-form-item>
+                            <el-form-item label="确认新密码:">
+                                <el-input type="password" v-model="form.new1"></el-input>
+                            </el-form-item>
+                            <el-form-item>
+                                <el-button type="primary" @click="onSubmit">保存</el-button>
+                            </el-form-item>
+                        </el-form>
+                    </el-tab-pane>
+                    <el-tab-pane name="label4" label="赞赏作者" class="user-tabpane">
+                        <div class="plugins-tips">
+                            如果该框架
+                            <el-link href="https://github.com/lin-xin/vue-manage-system" target="_blank"
+                                >vue-manage-system</el-link
+                            >
+                            对你有帮助,那就请作者喝杯饮料吧!<el-icon>
+                                <ColdDrink />
+                            </el-icon>
+                            加微信号 linxin_20 探讨问题。
+                        </div>
+                        <div>
+                            <img src="https://lin-xin.gitee.io/images/weixin.jpg" />
+                        </div>
+                    </el-tab-pane>
+                </el-tabs>
+            </el-card>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts" name="ucenter">
+import { reactive, ref } from 'vue';
+import { VueCropper } from 'vue-cropper';
+import 'vue-cropper/dist/index.css';
+import avatar from '@/assets/img/img.jpg';
+import TabsComp from '../element/tabs.vue';
+
+const name = localStorage.getItem('ms_username');
+const form = reactive({
+    new1: '',
+    new: '',
+    old: '',
+});
+const onSubmit = () => {};
+
+const activeName = ref('label1');
+
+const avatarImg = ref(avatar);
+const imgSrc = ref(avatar);
+const cropImg = ref('');
+const cropper: any = ref();
+
+const setImage = (e: any) => {
+    const file = e.target.files[0];
+    if (!file.type.includes('image/')) {
+        return;
+    }
+    const reader = new FileReader();
+    reader.onload = (event: any) => {
+        imgSrc.value = event.target.result;
+        cropper.value && cropper.value.replace(event.target.result);
+    };
+    reader.readAsDataURL(file);
+};
+
+const cropImage = () => {
+    cropImg.value = cropper.value?.getCroppedCanvas().toDataURL();
+};
+
+const saveAvatar = () => {
+    avatarImg.value = cropImg.value;
+};
+</script>
+
+<style scoped>
+.user-container {
+    display: flex;
+}
+
+.user-profile {
+    position: relative;
+}
+
+.user-profile-bg {
+    width: 100%;
+    height: 200px;
+    background-image: url('../../assets/img/ucenter-bg.jpg');
+    background-size: cover;
+    background-position: center;
+    background-repeat: no-repeat;
+}
+
+.user-profile {
+    width: 500px;
+    margin-right: 20px;
+    flex: 0 0 auto;
+    align-self: flex-start;
+}
+
+.user-avatar-wrap {
+    position: absolute;
+    top: 135px;
+    width: 100%;
+    text-align: center;
+}
+
+.user-avatar {
+    border: 5px solid #fff;
+    border-radius: 50%;
+    overflow: hidden;
+    box-shadow: 0 7px 12px 0 rgba(62, 57, 107, 0.16);
+}
+
+.user-info {
+    text-align: center;
+    padding: 80px 0 30px;
+}
+
+.info-name {
+    margin: 0 0 20px;
+    font-size: 22px;
+    font-weight: 500;
+    color: #373a3c;
+}
+
+.info-desc {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 5px;
+}
+
+.info-desc,
+.info-desc a {
+    font-size: 18px;
+    color: #55595c;
+}
+
+.info-icon {
+    margin-top: 10px;
+}
+
+.info-icon i {
+    font-size: 30px;
+    margin: 0 10px;
+    color: #343434;
+}
+
+.user-content {
+    flex: 1;
+}
+
+.user-tabpane {
+    padding: 10px 20px;
+}
+
+.crop-wrap {
+    width: 600px;
+    height: 350px;
+    margin-bottom: 20px;
+}
+
+.crop-demo-btn {
+    position: relative;
+}
+
+.crop-input {
+    position: absolute;
+    width: 100px;
+    height: 40px;
+    left: 0;
+    top: 0;
+    opacity: 0;
+    cursor: pointer;
+}
+
+.w500 {
+    width: 500px;
+}
+
+.user-footer {
+    display: flex;
+    border-top: 1px solid rgba(83, 70, 134, 0.1);
+}
+
+.user-footer-item {
+    padding: 20px 0;
+    width: 33.3333333333%;
+    text-align: center;
+}
+
+.user-footer > div + div {
+    border-left: 1px solid rgba(83, 70, 134, 0.1);
+}
+</style>
+
+<style>
+.el-tabs.el-tabs--left {
+    height: 100%;
+}
+</style>

+ 253 - 0
src/views/pages/ucenter2.vue

@@ -0,0 +1,253 @@
+<template>
+	<div class="user-container">
+		<el-card class="user-profile mgb20" shadow="hover" :body-style="{ padding: '0px' }">
+			<div class="user-profile-bg"></div>
+			<div class="user-avatar-wrap">
+				<el-avatar class="user-avatar" :size="120" :src="avatarImg" />
+			</div>
+			<div class="user-info">
+				<div class="info-name">{{ name }}</div>
+				<div class="info-desc">
+					<!-- <span>{{ name }}</span>
+					<el-divider direction="vertical" /> -->
+					<span>FE Developer</span>
+					<el-divider direction="vertical" />
+					<el-link href="https://lin-xin.gitee.io" target="_blank">lin-xin.gitee.io</el-link>
+				</div>
+				<!-- <div class="info-icon">
+					<a href="https://github.com/lin-xin" target="_blank"> <i class="el-icon-lx-github-fill"></i></a>
+					<i class="el-icon-lx-qq-fill"></i>
+					<i class="el-icon-lx-facebook-fill"></i>
+					<i class="el-icon-lx-twitter-fill"></i>
+				</div> -->
+			</div>
+			<!-- <div class="user-footer">
+				<div class="user-footer-item">
+					<el-statistic title="Follower" value="18K" />
+				</div>
+				<div class="user-footer-item">
+					<el-statistic title="Following" :value="666" />
+				</div>
+				<div class="user-footer-item">
+					<el-statistic title="Total Post" :value="888" />
+				</div>
+			</div> -->
+		</el-card>
+		<el-card class="user-content" shadow="hover"
+			:body-style="{ padding: '20px 50px', height: '100%', boxSizing: 'border-box' }">
+			<el-tabs v-model="activeName">
+
+				<el-tab-pane name="label1" label="消息通知" class="user-tabpane">
+					<TabsComp />
+				</el-tab-pane>
+				<el-tab-pane name="label2" label="我的头像" class="user-tabpane">
+					<div class="crop-wrap" v-if="activeName === 'label2'">
+						<vueCropper ref="cropper" :img="imgSrc" :autoCrop="true" :centerBox="true" :full="true"
+							mode="contain">
+						</vueCropper>
+					</div>
+					<el-button class="crop-demo-btn" type="primary">选择图片
+						<input class="crop-input" type="file" name="image" accept="image/*" @change="setImage" />
+					</el-button>
+					<el-button type="success" @click="saveAvatar">上传并保存</el-button>
+				</el-tab-pane>
+				<el-tab-pane name="label3" label="修改密码" class="user-tabpane">
+					<el-form class="w500" label-position="top">
+						<el-form-item label="旧密码:">
+							<el-input type="password" v-model="form.old"></el-input>
+						</el-form-item>
+						<el-form-item label="新密码:">
+							<el-input type="password" v-model="form.new"></el-input>
+						</el-form-item>
+						<el-form-item label="确认新密码:">
+							<el-input type="password" v-model="form.new1"></el-input>
+						</el-form-item>
+						<el-form-item>
+							<el-button type="primary" @click="onSubmit">保存</el-button>
+						</el-form-item>
+					</el-form>
+				</el-tab-pane>
+				<el-tab-pane name="label4" label="赞赏作者" class="user-tabpane">
+					<div class="plugins-tips">
+						如果该框架 <el-link href="https://github.com/lin-xin/vue-manage-system"
+							target="_blank">vue-manage-system</el-link> 对你有帮助,那就请作者喝杯饮料吧!<el-icon>
+							<ColdDrink />
+						</el-icon> 加微信号 linxin_20 探讨问题。
+					</div>
+					<div>
+						<img src="https://lin-xin.gitee.io/images/weixin.jpg" />
+					</div>
+				</el-tab-pane>
+			</el-tabs>
+		</el-card>
+	</div>
+</template>
+
+<script setup lang="ts" name="ucenter">
+import { reactive, ref } from 'vue';
+import { VueCropper } from "vue-cropper"
+import 'vue-cropper/dist/index.css'
+import avatar from '@/assets/img/img.jpg';
+import TabsComp from '../element/tabs.vue';
+
+const name = localStorage.getItem('ms_username');
+const form = reactive({
+	new1: '',
+	new: '',
+	old: ''
+});
+const onSubmit = () => { };
+
+const activeName = ref('label1');
+
+const avatarImg = ref(avatar);
+const imgSrc = ref(avatar);
+const cropImg = ref('');
+const cropper: any = ref();
+
+const setImage = (e: any) => {
+	const file = e.target.files[0];
+	if (!file.type.includes('image/')) {
+		return;
+	}
+	const reader = new FileReader();
+	reader.onload = (event: any) => {
+		imgSrc.value = event.target.result;
+		cropper.value && cropper.value.replace(event.target.result);
+	};
+	reader.readAsDataURL(file);
+};
+
+const cropImage = () => {
+	cropImg.value = cropper.value?.getCroppedCanvas().toDataURL();
+};
+
+const saveAvatar = () => {
+	avatarImg.value = cropImg.value;
+};
+</script>
+
+<style scoped>
+/* .user-container {
+	display: flex;
+} */
+
+.user-profile-bg {
+	width: 100%;
+	height: 150px;
+	background-image: url('../../assets/img/bahnhofsidylle.jpg');
+	background-size: cover;
+	background-position: center;
+	background-repeat: no-repeat;
+}
+
+.user-profile {
+	position: relative;
+	/* width: 500px; */
+	/* margin-right: 20px; */
+	/* flex: 0 0 auto;
+	align-self: flex-start;
+} */
+}
+
+.user-avatar-wrap {
+	position: absolute;
+	top: 90px;
+	width: 100%;
+	text-align: center;
+}
+
+.user-avatar {
+	border: 5px solid #fff;
+	border-radius: 50%;
+	overflow: hidden;
+	box-shadow: 0 7px 12px 0 rgba(62, 57, 107, .16)
+}
+
+.user-info {
+	text-align: center;
+	padding: 70px 0 20px;
+}
+
+.info-name {
+	margin: 0 0 10px;
+	font-size: 22px;
+	font-weight: 500;
+	color: #373a3c;
+}
+
+.info-desc {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.info-desc,
+.info-desc a {
+	font-size: 18px;
+	color: #55595c;
+}
+
+.info-icon {
+	margin-top: 10px;
+}
+
+.info-icon i {
+	font-size: 30px;
+	margin: 0 10px;
+	color: #343434;
+}
+
+.user-content {
+	flex: 1
+}
+
+.user-tabpane {
+	padding: 10px 20px;
+}
+
+.crop-wrap {
+	width: 600px;
+	height: 350px;
+	margin-bottom: 20px;
+}
+
+.crop-demo-btn {
+	position: relative;
+}
+
+.crop-input {
+	position: absolute;
+	width: 100px;
+	height: 40px;
+	left: 0;
+	top: 0;
+	opacity: 0;
+	cursor: pointer;
+}
+
+.w500 {
+	width: 500px;
+}
+
+.user-footer {
+	display: flex;
+	border-top: 1px solid rgba(83, 70, 134, 0.1);
+}
+
+.user-footer-item {
+	padding: 20px 0;
+	width: 33.3333333333%;
+	text-align: center;
+}
+
+.user-footer>div+div {
+	border-left: 1px solid rgba(83, 70, 134, 0.1);
+}
+</style>
+
+<style>
+.el-tabs.el-tabs--left {
+	height: 100%;
+}
+</style>

+ 0 - 137
src/views/permission.vue

@@ -1,137 +0,0 @@
-<template>
-	<div class="container">
-		<div class="plugins-tips">通过 v-permiss 自定义指令实现权限管理,使用非 admin 账号登录,可查看效果。</div>
-		<div class="mgb20">
-			<span class="label">角色:</span>
-			<el-select v-model="role" @change="handleChange">
-				<el-option label="超级管理员" value="admin"></el-option>
-				<el-option label="普通用户" value="user"></el-option>
-			</el-select>
-		</div>
-		<div class="mgb20 tree-wrapper">
-			<el-tree
-				ref="tree"
-				:data="data"
-				node-key="id"
-				default-expand-all
-				show-checkbox
-				:default-checked-keys="checkedKeys"
-			/>
-		</div>
-		<el-button type="primary" @click="onSubmit">保存权限</el-button>
-	</div>
-</template>
-
-<script setup lang="ts" name="permission">
-import { ref } from 'vue';
-import { ElTree } from 'element-plus';
-import { usePermissStore } from '../store/permiss';
-
-const role = ref<string>('admin');
-
-interface Tree {
-	id: string;
-	label: string;
-	children?: Tree[];
-}
-
-const data: Tree[] = [
-	{
-		id: '1',
-		label: '系统首页'
-	},
-	{
-		id: '2',
-		label: '基础表格',
-		children: [
-			{
-				id: '15',
-				label: '编辑'
-			},
-			{
-				id: '16',
-				label: '删除'
-			}
-		]
-	},
-	{
-		id: '3',
-		label: 'tab选项卡'
-	},
-	{
-		id: '4',
-		label: '表单相关',
-		children: [
-			{
-				id: '5',
-				label: '基本表单'
-			},
-			{
-				id: '6',
-				label: '文件上传'
-			},
-			{
-				id: '7',
-				label: '三级菜单',
-				children: [
-					{
-						id: '8',
-						label: '富文本编辑器'
-					},
-					{
-						id: '9',
-						label: 'markdown编辑器'
-					}
-				]
-			}
-		]
-	},
-	{
-		id: '10',
-		label: '自定义图标'
-	},
-	{
-		id: '11',
-		label: 'schart图表'
-	},
-
-	{
-		id: '13',
-		label: '权限管理'
-	},
-	{
-		id: '14',
-		label: '支持作者'
-	}
-];
-
-const permiss = usePermissStore();
-
-// 获取当前权限
-const checkedKeys = ref<string[]>([]);
-const getPremission = () => {
-	// 请求接口返回权限
-	checkedKeys.value = permiss.defaultList[role.value];
-};
-getPremission();
-
-// 保存权限
-const tree = ref<InstanceType<typeof ElTree>>();
-const onSubmit = () => {
-	// 获取选中的权限
-	console.log(tree.value!.getCheckedKeys(false));
-};
-
-const handleChange = (val: string[]) => {
-	tree.value!.setCheckedKeys(permiss.defaultList[role.value]);
-};
-</script>
-
-<style scoped>
-.tree-wrapper {
-	max-width: 500px;
-}
-.label {
-	font-size: 14px;
-}
-</style>

+ 144 - 0
src/views/system/menu.vue

@@ -0,0 +1,144 @@
+<template>
+    <div>
+        <div class="container">
+            <TableCustom :columns="columns" :tableData="menuData" row-key="index" :has-pagination="false"
+                :viewFunc="handleView" :delFunc="handleDelete" :editFunc="handleEdit">
+                <template #toolbarBtn>
+                    <el-button type="warning" :icon="CirclePlusFilled" @click="visible = true">新增</el-button>
+                </template>
+                <template #icon="{ rows }">
+                    <el-icon>
+                        <component :is="rows.icon"></component>
+                    </el-icon>
+                </template>
+            </TableCustom>
+
+        </div>
+        <el-dialog :title="isEdit ? '编辑' : '新增'" v-model="visible" width="700px" destroy-on-close
+            :close-on-click-modal="false" @close="closeDialog">
+            <TableEdit :form-data="rowData" :options="options" :edit="isEdit" :update="updateData">
+                <template #parent>
+                    <el-cascader v-model="rowData.pid" :options="cascaderOptions" :props="{ checkStrictly: true }"
+                        clearable />
+                </template>
+            </TableEdit>
+        </el-dialog>
+        <el-dialog title="查看详情" v-model="visible1" width="700px" destroy-on-close>
+            <TableDetail :data="viewData">
+                <template #icon="{ rows }">
+                    <el-icon>
+                        <component :is="rows.icon"></component>
+                    </el-icon>
+                </template>
+            </TableDetail>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup lang="ts" name="system-menu">
+import { ref } from 'vue';
+import { ElMessage } from 'element-plus';
+import { CirclePlusFilled } from '@element-plus/icons-vue';
+import { Menus } from '@/types/menu';
+import TableCustom from '@/components/table-custom.vue';
+import TableDetail from '@/components/table-detail.vue';
+import { FormOption } from '@/types/form-option';
+import { menuData } from '@/components/menu';
+
+// 表格相关
+let columns = ref([
+    { prop: 'title', label: '菜单名称', align: 'left' },
+    { prop: 'icon', label: '图标' },
+    { prop: 'index', label: '路由路径' },
+    { prop: 'permiss', label: '权限标识' },
+    { prop: 'operator', label: '操作', width: 250 },
+])
+
+const getOptions = (data: any) => {
+    return data.map(item => {
+        const a: any = {
+            label: item.title,
+            value: item.id,
+        }
+        if (item.children) {
+            a.children = getOptions(item.children)
+        }
+        return a
+    })
+}
+const cascaderOptions = ref(getOptions(menuData));
+
+
+// 新增/编辑弹窗相关
+let options = ref<FormOption>({
+    labelWidth: '100px',
+    span: 12,
+    list: [
+        { type: 'input', label: '菜单名称', prop: 'title', required: true },
+        { type: 'input', label: '路由路径', prop: 'index', required: true },
+        { type: 'input', label: '图标', prop: 'icon' },
+        { type: 'input', label: '权限标识', prop: 'permiss' },
+        { type: 'parent', label: '父菜单', prop: 'parent' },
+    ]
+})
+const visible = ref(false);
+const isEdit = ref(false);
+const rowData = ref<any>({});
+const handleEdit = (row: Menus) => {
+    rowData.value = { ...row };
+    isEdit.value = true;
+    visible.value = true;
+};
+const updateData = () => {
+    closeDialog();
+};
+
+const closeDialog = () => {
+    visible.value = false;
+    isEdit.value = false;
+};
+
+// 查看详情弹窗相关
+const visible1 = ref(false);
+const viewData = ref({
+    row: {},
+    list: []
+});
+const handleView = (row: Menus) => {
+    viewData.value.row = { ...row }
+    viewData.value.list = [
+        {
+            prop: 'id',
+            label: '菜单ID',
+        },
+        {
+            prop: 'pid',
+            label: '父菜单ID',
+        },
+        {
+            prop: 'title',
+            label: '菜单名称',
+        },
+        {
+            prop: 'index',
+            label: '路由路径',
+        },
+        {
+            prop: 'permiss',
+            label: '权限标识',
+        },
+        {
+            prop: 'icon',
+            label: '图标',
+        },
+    ]
+    visible1.value = true;
+};
+
+// 删除相关
+const handleDelete = (row: Menus) => {
+    ElMessage.success('删除成功');
+}
+</script>
+
+<style scoped></style>

+ 76 - 0
src/views/system/role-permission.vue

@@ -0,0 +1,76 @@
+<template>
+    <div>
+        <el-tree
+            class="mgb10"
+            ref="tree"
+            :data="data"
+            node-key="id"
+            default-expand-all
+            show-checkbox
+            :default-checked-keys="checkedKeys"
+        />
+        <el-button type="primary" @click="onSubmit">保存权限</el-button>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { ElTree } from 'element-plus';
+import { menuData } from '@/components/menu';
+
+const props = defineProps({
+    permissOptions: {
+        type: Object,
+        required: true,
+    },
+});
+
+const menuObj = ref({});
+// const data = menuData.map((item) => {
+//     if (item.children) {
+//         menuObj.value[item.id] = item.children.map((sub) => sub.id);
+//     }
+//     return {
+//         id: item.id,
+//         label: item.title,
+//         children: item.children?.map((child) => {
+//             return {
+//                 id: child.id,
+//                 label: child.title,
+//             };
+//         }),
+//     };
+// });
+
+const getTreeData = (data) => {
+    return data.map((item) => {
+        const obj: any = {
+            id: item.id,
+            label: item.title,
+        };
+        if (item.children) {
+            menuObj.value[item.id] = item.children.map((sub) => sub.id);
+            obj.children = getTreeData(item.children);
+        }
+        return obj;
+    });
+};
+const data = getTreeData(menuData);
+const checkData = (data: string[]) => {
+    return data.filter((item) => {
+        return !menuObj.value[item] || data.toString().includes(menuObj.value[item].toString());
+    });
+};
+// 获取当前权限
+const checkedKeys = ref<string[]>(checkData(props.permissOptions.permiss));
+
+// 保存权限
+const tree = ref<InstanceType<typeof ElTree>>();
+const onSubmit = () => {
+    // 获取选中的权限
+    const keys = [...tree.value!.getCheckedKeys(false), ...tree.value!.getHalfCheckedKeys()] as number[];
+    console.log(keys);
+};
+</script>
+
+<style scoped></style>

+ 162 - 0
src/views/system/role.vue

@@ -0,0 +1,162 @@
+<template>
+    <div>
+        <TableSearch :query="query" :options="searchOpt" :search="handleSearch" />
+        <div class="container">
+
+            <TableCustom :columns="columns" :tableData="tableData" :total="page.total" :viewFunc="handleView"
+                :delFunc="handleDelete" :page-change="changePage" :editFunc="handleEdit">
+                <template #toolbarBtn>
+                    <el-button type="warning" :icon="CirclePlusFilled" @click="visible = true">新增</el-button>
+                </template>
+                <template #status="{ rows }">
+                    <el-tag type="success" v-if="rows.status">启用</el-tag>
+                    <el-tag type="danger" v-else>禁用</el-tag>
+                </template>
+                <template #permissions="{ rows }">
+                    <el-button type="primary" size="small" plain @click="handlePermission(rows)">管理</el-button>
+                </template>
+            </TableCustom>
+        </div>
+        <el-dialog :title="isEdit ? '编辑' : '新增'" v-model="visible" width="700px" destroy-on-close
+            :close-on-click-modal="false" @close="closeDialog">
+            <TableEdit :form-data="rowData" :options="options" :edit="isEdit" :update="updateData" />
+        </el-dialog>
+        <el-dialog title="查看详情" v-model="visible1" width="700px" destroy-on-close>
+            <TableDetail :data="viewData">
+                <template #status="{ rows }">
+                    <el-tag type="success" v-if="rows.status">启用</el-tag>
+                    <el-tag type="danger" v-else>禁用</el-tag>
+                </template>
+            </TableDetail>
+        </el-dialog>
+        <el-dialog title="权限管理" v-model="visible2" width="500px" destroy-on-close>
+            <RolePermission :permiss-options="permissOptions" />
+        </el-dialog>
+    </div>
+</template>
+
+<script setup lang="ts" name="system-role">
+import { ref, reactive } from 'vue';
+import { ElMessage } from 'element-plus';
+import { Role } from '@/types/role';
+import { fetchRoleData } from '@/api';
+import TableCustom from '@/components/table-custom.vue';
+import TableDetail from '@/components/table-detail.vue';
+import RolePermission from './role-permission.vue'
+import { CirclePlusFilled } from '@element-plus/icons-vue';
+import { FormOption, FormOptionList } from '@/types/form-option';
+
+// 查询相关
+const query = reactive({
+    name: '',
+});
+const searchOpt = ref<FormOptionList[]>([
+    { type: 'input', label: '角色名称:', prop: 'name' }
+])
+const handleSearch = () => {
+    changePage(1);
+};
+
+// 表格相关
+let columns = ref([
+    { type: 'index', label: '序号', width: 55, align: 'center' },
+    { prop: 'name', label: '角色名称' },
+    { prop: 'key', label: '角色标识' },
+    { prop: 'status', label: '状态' },
+    { prop: 'permissions', label: '权限管理' },
+    { prop: 'operator', label: '操作', width: 250 },
+])
+const page = reactive({
+    index: 1,
+    size: 10,
+    total: 0,
+})
+const tableData = ref<Role[]>([]);
+const getData = async () => {
+    const res = await fetchRoleData()
+    tableData.value = res.data.list;
+    page.total = res.data.pageTotal;
+};
+getData();
+const changePage = (val: number) => {
+    page.index = val;
+    getData();
+};
+
+// 新增/编辑弹窗相关
+const options = ref<FormOption>({
+    labelWidth: '100px',
+    span: 24,
+    list: [
+        { type: 'input', label: '角色名称', prop: 'name', required: true },
+        { type: 'input', label: '角色标识', prop: 'key', required: true },
+        { type: 'switch', label: '状态', prop: 'status', required: false, activeText: '启用', inactiveText: '禁用' },
+    ]
+})
+const visible = ref(false);
+const isEdit = ref(false);
+const rowData = ref({});
+const handleEdit = (row: Role) => {
+    rowData.value = { ...row };
+    isEdit.value = true;
+    visible.value = true;
+};
+const updateData = () => {
+    closeDialog();
+    getData();
+};
+const closeDialog = () => {
+    visible.value = false;
+    isEdit.value = false;
+    rowData.value = {};
+};
+
+// 查看详情弹窗相关
+const visible1 = ref(false);
+const viewData = ref({
+    row: {},
+    list: [],
+    column: 1
+});
+const handleView = (row: Role) => {
+    viewData.value.row = { ...row }
+    viewData.value.list = [
+        {
+            prop: 'id',
+            label: '角色ID',
+        },
+        {
+            prop: 'name',
+            label: '角色名称',
+        },
+        {
+            prop: 'key',
+            label: '角色标识',
+        },
+        {
+            prop: 'status',
+            label: '角色状态',
+        },
+    ]
+    visible1.value = true;
+};
+
+// 删除相关
+const handleDelete = (row: Role) => {
+    ElMessage.success('删除成功');
+}
+
+
+// 权限管理弹窗相关
+const visible2 = ref(false);
+const permissOptions = ref({})
+const handlePermission = (row: Role) => {
+    visible2.value = true;
+    permissOptions.value = {
+        id: row.id,
+        permiss: row.permiss
+    };
+}
+</script>
+
+<style scoped></style>

+ 148 - 0
src/views/system/user.vue

@@ -0,0 +1,148 @@
+<template>
+    <div>
+        <TableSearch :query="query" :options="searchOpt" :search="handleSearch" />
+        <div class="container">
+            <TableCustom :columns="columns" :tableData="tableData" :total="page.total" :viewFunc="handleView"
+                :delFunc="handleDelete" :page-change="changePage" :editFunc="handleEdit">
+                <template #toolbarBtn>
+                    <el-button type="warning" :icon="CirclePlusFilled" @click="visible = true">新增</el-button>
+                </template>
+            </TableCustom>
+
+        </div>
+        <el-dialog :title="isEdit ? '编辑' : '新增'" v-model="visible" width="700px" destroy-on-close
+            :close-on-click-modal="false" @close="closeDialog">
+            <TableEdit :form-data="rowData" :options="options" :edit="isEdit" :update="updateData" />
+        </el-dialog>
+        <el-dialog title="查看详情" v-model="visible1" width="700px" destroy-on-close>
+            <TableDetail :data="viewData"></TableDetail>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup lang="ts" name="system-user">
+import { ref, reactive } from 'vue';
+import { ElMessage } from 'element-plus';
+import { CirclePlusFilled } from '@element-plus/icons-vue';
+import { User } from '@/types/user';
+import { fetchUserData } from '@/api';
+import TableCustom from '@/components/table-custom.vue';
+import TableDetail from '@/components/table-detail.vue';
+import TableSearch from '@/components/table-search.vue';
+import { FormOption, FormOptionList } from '@/types/form-option';
+
+// 查询相关
+const query = reactive({
+    name: '',
+});
+const searchOpt = ref<FormOptionList[]>([
+    { type: 'input', label: '用户名:', prop: 'name' }
+])
+const handleSearch = () => {
+    changePage(1);
+};
+
+// 表格相关
+let columns = ref([
+    { type: 'index', label: '序号', width: 55, align: 'center' },
+    { prop: 'name', label: '用户名' },
+    { prop: 'phone', label: '手机号' },
+    { prop: 'role', label: '角色' },
+    { prop: 'operator', label: '操作', width: 250 },
+])
+const page = reactive({
+    index: 1,
+    size: 10,
+    total: 0,
+})
+const tableData = ref<User[]>([]);
+const getData = async () => {
+    const res = await fetchUserData()
+    tableData.value = res.data.list;
+    page.total = res.data.pageTotal;
+};
+getData();
+
+const changePage = (val: number) => {
+    page.index = val;
+    getData();
+};
+
+// 新增/编辑弹窗相关
+let options = ref<FormOption>({
+    labelWidth: '100px',
+    span: 12,
+    list: [
+        { type: 'input', label: '用户名', prop: 'name', required: true },
+        { type: 'input', label: '手机号', prop: 'phone', required: true },
+        { type: 'input', label: '密码', prop: 'password', required: true },
+        { type: 'input', label: '邮箱', prop: 'email', required: true },
+        { type: 'input', label: '角色', prop: 'role', required: true },
+    ]
+})
+const visible = ref(false);
+const isEdit = ref(false);
+const rowData = ref({});
+const handleEdit = (row: User) => {
+    rowData.value = { ...row };
+    isEdit.value = true;
+    visible.value = true;
+};
+const updateData = () => {
+    closeDialog();
+    getData();
+};
+
+const closeDialog = () => {
+    visible.value = false;
+    isEdit.value = false;
+};
+
+// 查看详情弹窗相关
+const visible1 = ref(false);
+const viewData = ref({
+    row: {},
+    list: []
+});
+const handleView = (row: User) => {
+    viewData.value.row = { ...row }
+    viewData.value.list = [
+        {
+            prop: 'id',
+            label: '用户ID',
+        },
+        {
+            prop: 'name',
+            label: '用户名',
+        },
+        {
+            prop: 'password',
+            label: '密码',
+        },
+        {
+            prop: 'email',
+            label: '邮箱',
+        },
+        {
+            prop: 'phone',
+            label: '电话',
+        },
+        {
+            prop: 'role',
+            label: '角色',
+        },
+        {
+            prop: 'date',
+            label: '注册日期',
+        },
+    ]
+    visible1.value = true;
+};
+
+// 删除相关
+const handleDelete = (row: User) => {
+    ElMessage.success('删除成功');
+}
+</script>
+
+<style scoped></style>

+ 0 - 194
src/views/table.vue

@@ -1,194 +0,0 @@
-<template>
-	<div>
-		<div class="container">
-			<div class="search-box">
-				<el-input v-model="query.name" placeholder="用户名" class="search-input mr10" clearable></el-input>
-				<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
-				<el-button type="warning" :icon="CirclePlusFilled" @click="visible = true">新增</el-button>
-			</div>
-			<el-table :data="tableData" border class="table" ref="multipleTable" header-cell-class-name="table-header">
-				<el-table-column prop="id" label="ID" width="55" align="center"></el-table-column>
-				<el-table-column prop="name" label="用户名" align="center"></el-table-column>
-				<el-table-column label="账户余额" align="center">
-					<template #default="scope">¥{{ scope.row.money }}</template>
-				</el-table-column>
-				<el-table-column label="头像(查看大图)" align="center">
-					<template #default="scope">
-						<el-image
-							class="table-td-thumb"
-							:src="scope.row.thumb"
-							:z-index="10"
-							:preview-src-list="[scope.row.thumb]"
-							preview-teleported
-						>
-						</el-image>
-					</template>
-				</el-table-column>
-				<el-table-column prop="address" label="地址" align="center"></el-table-column>
-				<el-table-column label="账户状态" align="center">
-					<template #default="scope">
-						<el-tag :type="scope.row.state ? 'success' : 'danger'">
-							{{ scope.row.state ? '正常' : '异常' }}
-						</el-tag>
-					</template>
-				</el-table-column>
-
-				<el-table-column prop="date" label="注册时间" align="center"></el-table-column>
-				<el-table-column label="操作" width="280" align="center">
-					<template #default="scope">
-						<el-button type="warning" size="small" :icon="View" @click="handleView(scope.row)">
-							查看
-						</el-button>
-						<el-button
-							type="primary"
-							size="small"
-							:icon="Edit"
-							@click="handleEdit(scope.$index, scope.row)"
-							v-permiss="15"
-						>
-							编辑
-						</el-button>
-						<el-button
-							type="danger"
-							size="small"
-							:icon="Delete"
-							@click="handleDelete(scope.$index)"
-							v-permiss="16"
-						>
-							删除
-						</el-button>
-					</template>
-				</el-table-column>
-			</el-table>
-			<div class="pagination">
-				<el-pagination
-					background
-					layout="total, prev, pager, next"
-					:current-page="query.pageIndex"
-					:page-size="query.pageSize"
-					:total="pageTotal"
-					@current-change="handlePageChange"
-				></el-pagination>
-			</div>
-		</div>
-		<el-dialog
-			:title="idEdit ? '编辑用户' : '新增用户'"
-			v-model="visible"
-			width="500px"
-			destroy-on-close
-			:close-on-click-modal="false"
-			@close="closeDialog"
-		>
-			<TableEdit :data="rowData" :edit="idEdit" :update="updateData" />
-		</el-dialog>
-		<el-dialog title="查看用户详情" v-model="visible1" width="700px" destroy-on-close>
-			<TableDetail :data="rowData" />
-		</el-dialog>
-	</div>
-</template>
-
-<script setup lang="ts" name="basetable">
-import { ref, reactive } from 'vue';
-import { ElMessage, ElMessageBox } from 'element-plus';
-import { Delete, Edit, Search, CirclePlusFilled, View } from '@element-plus/icons-vue';
-import { fetchData } from '../api/index';
-import TableEdit from '../components/table-edit.vue';
-import TableDetail from '../components/table-detail.vue';
-
-interface TableItem {
-	id: number;
-	name: string;
-	thumb: string;
-	money: number;
-	state: string;
-	date: string;
-	address: string;
-}
-
-const query = reactive({
-	address: '',
-	name: '',
-	pageIndex: 1,
-	pageSize: 10
-});
-const tableData = ref<TableItem[]>([]);
-const pageTotal = ref(0);
-// 获取表格数据
-const getData = async () => {
-	const res = await fetchData();
-	tableData.value = res.data.list;
-	pageTotal.value = res.data.pageTotal || 50;
-};
-getData();
-
-// 查询操作
-const handleSearch = () => {
-	query.pageIndex = 1;
-	getData();
-};
-// 分页导航
-const handlePageChange = (val: number) => {
-	query.pageIndex = val;
-	getData();
-};
-
-// 删除操作
-const handleDelete = (index: number) => {
-	// 二次确认删除
-	ElMessageBox.confirm('确定要删除吗?', '提示', {
-		type: 'warning'
-	})
-		.then(() => {
-			ElMessage.success('删除成功');
-			tableData.value.splice(index, 1);
-		})
-		.catch(() => {});
-};
-
-const visible = ref(false);
-let idx: number = -1;
-const idEdit = ref(false);
-const rowData = ref({});
-const handleEdit = (index: number, row: TableItem) => {
-	idx = index;
-	rowData.value = row;
-	idEdit.value = true;
-	visible.value = true;
-};
-const updateData = (row: TableItem) => {
-	idEdit.value ? (tableData.value[idx] = row) : tableData.value.unshift(row);
-	console.log(tableData.value);
-	closeDialog();
-};
-
-const closeDialog = () => {
-	visible.value = false;
-	idEdit.value = false;
-};
-
-const visible1 = ref(false);
-const handleView = (row: TableItem) => {
-	rowData.value = row;
-	visible1.value = true;
-};
-</script>
-
-<style scoped>
-.search-box {
-	margin-bottom: 20px;
-}
-
-.search-input {
-	width: 200px;
-}
-
-.mr10 {
-	margin-right: 10px;
-}
-.table-td-thumb {
-	display: block;
-	margin: auto;
-	width: 40px;
-	height: 40px;
-}
-</style>

+ 169 - 0
src/views/table/basetable.vue

@@ -0,0 +1,169 @@
+<template>
+	<div>
+		<TableSearch :query="query" :options="searchOpt" :search="handleSearch" />
+		<div class="container">
+			<TableCustom :columns="columns" :tableData="tableData" :total="page.total" :viewFunc="handleView"
+				:delFunc="handleDelete" :editFunc="handleEdit" :refresh="getData" :currentPage="page.index"
+				:changePage="changePage">
+				<template #toolbarBtn>
+					<el-button type="warning" :icon="CirclePlusFilled" @click="visible = true">新增</el-button>
+				</template>
+				<template #money="{ rows }">
+					¥{{ rows.money }}
+				</template>
+				<template #thumb="{ rows }">
+					<el-image class="table-td-thumb" :src="rows.thumb" :z-index="10" :preview-src-list="[rows.thumb]"
+						preview-teleported>
+					</el-image>
+				</template>
+				<template #state="{ rows }">
+					<el-tag :type="rows.state ? 'success' : 'danger'">
+						{{ rows.state ? '正常' : '异常' }}
+					</el-tag>
+				</template>
+			</TableCustom>
+
+		</div>
+		<el-dialog :title="isEdit ? '编辑' : '新增'" v-model="visible" width="700px" destroy-on-close
+			:close-on-click-modal="false" @close="closeDialog">
+			<TableEdit :form-data="rowData" :options="options" :edit="isEdit" :update="updateData">
+				<template #thumb="{ rows }">
+					<img class="table-td-thumb" :src="rows.thumb"></img>
+				</template>
+			</TableEdit>
+		</el-dialog>
+		<el-dialog title="查看详情" v-model="visible1" width="700px" destroy-on-close>
+			<TableDetail :data="viewData">
+				<template #thumb="{ rows }">
+					<el-image :src="rows.thumb"></el-image>
+				</template>
+			</TableDetail>
+		</el-dialog>
+	</div>
+</template>
+
+<script setup lang="ts" name="basetable">
+import { ref, reactive } from 'vue';
+import { ElMessage, } from 'element-plus';
+import { CirclePlusFilled } from '@element-plus/icons-vue';
+import { fetchData } from '@/api/index';
+import TableCustom from '@/components/table-custom.vue';
+import TableDetail from '@/components/table-detail.vue';
+import TableSearch from '@/components/table-search.vue';
+import { TableItem } from '@/types/table';
+import { FormOption, FormOptionList } from '@/types/form-option';
+
+// 查询相关
+const query = reactive({
+	name: '',
+});
+const searchOpt = ref<FormOptionList[]>([
+	{ type: 'input', label: '用户名:', prop: 'name' }
+])
+const handleSearch = () => {
+	changePage(1);
+};
+
+// 表格相关
+let columns = ref([
+	{ type: 'selection' },
+	{ type: 'index', label: '序号', width: 55, align: 'center' },
+	{ prop: 'name', label: '用户名' },
+	{ prop: 'money', label: '账户余额' },
+	{ prop: 'thumb', label: '头像' },
+	{ prop: 'state', label: '账户状态' },
+	{ prop: 'operator', label: '操作', width: 250 },
+])
+const page = reactive({
+	index: 1,
+	size: 10,
+	total: 200,
+})
+const tableData = ref<TableItem[]>([]);
+const getData = async () => {
+	const res = await fetchData()
+	tableData.value = res.data.list;
+};
+getData();
+
+const changePage = (val: number) => {
+	page.index = val;
+	getData();
+};
+
+// 新增/编辑弹窗相关
+let options = ref<FormOption>({
+	labelWidth: '100px',
+	span: 24,
+	list: [
+		{ type: 'input', label: '用户名', prop: 'name', required: true },
+		{ type: 'number', label: '账户余额', prop: 'money', required: true },
+		{ type: 'switch', activeText: '正常', inactiveText: '异常', label: '账户状态', prop: 'state', required: true },
+		{ type: 'upload', label: '头像', prop: 'thumb', required: true },
+	]
+})
+const visible = ref(false);
+const isEdit = ref(false);
+const rowData = ref({});
+const handleEdit = (row: TableItem) => {
+	rowData.value = { ...row };
+	isEdit.value = true;
+	visible.value = true;
+};
+const updateData = () => {
+	closeDialog();
+	getData();
+};
+
+const closeDialog = () => {
+	visible.value = false;
+	isEdit.value = false;
+};
+
+// 查看详情弹窗相关
+const visible1 = ref(false);
+const viewData = ref({
+	row: {},
+	list: []
+});
+const handleView = (row: TableItem) => {
+	viewData.value.row = { ...row }
+	viewData.value.list = [
+		{
+			prop: 'id',
+			label: '用户ID',
+		},
+		{
+			prop: 'name',
+			label: '用户名',
+		},
+		{
+			prop: 'money',
+			label: '账户余额',
+		},
+		{
+			prop: 'state',
+			label: '账户状态',
+		},
+		{
+			prop: 'thumb',
+			label: '头像',
+		},
+	]
+	visible1.value = true;
+};
+
+// 删除相关
+const handleDelete = (row: TableItem) => {
+	ElMessage.success('删除成功');
+}
+</script>
+
+<style scoped>
+.table-td-thumb {
+	display: block;
+	margin: auto;
+	width: 40px;
+	height: 40px;
+}
+</style>

+ 98 - 98
src/views/export.vue → src/views/table/export.vue

@@ -1,98 +1,98 @@
-<template>
-    <div>
-        <div class="container">
-            <div class="handle-box">
-                <el-button type="primary" @click="exportXlsx">导出Excel</el-button>
-            </div>
-            <el-table :data="tableData" border class="table" header-cell-class-name="table-header">
-                <el-table-column prop="id" label="ID" width="55" align="center"></el-table-column>
-                <el-table-column prop="name" label="姓名"></el-table-column>
-                <el-table-column prop="sno" label="学号"></el-table-column>
-                <el-table-column prop="class" label="班级"></el-table-column>
-                <el-table-column prop="age" label="年龄"></el-table-column>
-                <el-table-column prop="sex" label="性别"></el-table-column>
-            </el-table>
-        </div>
-    </div>
-</template>
-
-<script setup lang="ts" name="export">
-import { ref } from 'vue';
-import * as XLSX from 'xlsx';
-
-interface TableItem {
-    id: number;
-    name: string;
-    sno: string;
-    class: string;
-    age: string;
-    sex: string;
-}
-
-const tableData = ref<TableItem[]>([]);
-// 获取表格数据
-const getData = () => {
-    tableData.value = [
-        {
-            id: 1,
-            name: '小明',
-            sno: 'S001',
-            class: '一班',
-            age: '10',
-            sex: '男',
-        },
-        {
-            id: 2,
-            name: '小红',
-            sno: 'S002',
-            class: '一班',
-            age: '9',
-            sex: '女',
-        },
-    ];
-};
-getData();
-
-const list = [['序号', '姓名', '学号', '班级', '年龄', '性别']];
-const exportXlsx = () => {
-    tableData.value.map((item: any, i: number) => {
-        const arr: any[] = [i + 1];
-        arr.push(...[item.name, item.sno, item.class, item.age, item.sex]);
-        list.push(arr);
-    });
-    let WorkSheet = XLSX.utils.aoa_to_sheet(list);
-    let new_workbook = XLSX.utils.book_new();
-    XLSX.utils.book_append_sheet(new_workbook, WorkSheet, '第一页');
-    XLSX.writeFile(new_workbook, `表格.xlsx`);
-};
-</script>
-
-<style scoped>
-.handle-box {
-    margin-bottom: 20px;
-}
-
-.handle-select {
-    width: 120px;
-}
-
-.handle-input {
-    width: 300px;
-}
-.table {
-    width: 100%;
-    font-size: 14px;
-}
-.red {
-    color: #f56c6c;
-}
-.mr10 {
-    margin-right: 10px;
-}
-.table-td-thumb {
-    display: block;
-    margin: auto;
-    width: 40px;
-    height: 40px;
-}
-</style>
+<template>
+    <div>
+        <div class="container">
+            <div class="handle-box">
+                <el-button type="primary" @click="exportXlsx">导出Excel</el-button>
+            </div>
+            <el-table :data="tableData" border class="table" header-cell-class-name="table-header">
+                <el-table-column prop="id" label="ID" width="55" align="center"></el-table-column>
+                <el-table-column prop="name" label="姓名"></el-table-column>
+                <el-table-column prop="sno" label="学号"></el-table-column>
+                <el-table-column prop="class" label="班级"></el-table-column>
+                <el-table-column prop="age" label="年龄"></el-table-column>
+                <el-table-column prop="sex" label="性别"></el-table-column>
+            </el-table>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts" name="export">
+import { ref } from 'vue';
+import * as XLSX from 'xlsx';
+
+interface TableItem {
+    id: number;
+    name: string;
+    sno: string;
+    class: string;
+    age: string;
+    sex: string;
+}
+
+const tableData = ref<TableItem[]>([]);
+// 获取表格数据
+const getData = () => {
+    tableData.value = [
+        {
+            id: 1,
+            name: '小明',
+            sno: 'S001',
+            class: '一班',
+            age: '10',
+            sex: '男',
+        },
+        {
+            id: 2,
+            name: '小红',
+            sno: 'S002',
+            class: '一班',
+            age: '9',
+            sex: '女',
+        },
+    ];
+};
+getData();
+
+const list = [['序号', '姓名', '学号', '班级', '年龄', '性别']];
+const exportXlsx = () => {
+    tableData.value.map((item: any, i: number) => {
+        const arr: any[] = [i + 1];
+        arr.push(...[item.name, item.sno, item.class, item.age, item.sex]);
+        list.push(arr);
+    });
+    let WorkSheet = XLSX.utils.aoa_to_sheet(list);
+    let new_workbook = XLSX.utils.book_new();
+    XLSX.utils.book_append_sheet(new_workbook, WorkSheet, '第一页');
+    XLSX.writeFile(new_workbook, `表格.xlsx`);
+};
+</script>
+
+<style scoped>
+.handle-box {
+    margin-bottom: 20px;
+}
+
+.handle-select {
+    width: 120px;
+}
+
+.handle-input {
+    width: 300px;
+}
+
+.table {
+    width: 100%;
+    font-size: 14px;
+}
+
+.red {
+    color: #f56c6c;
+}
+
+.table-td-thumb {
+    display: block;
+    margin: auto;
+    width: 40px;
+    height: 40px;
+}
+</style>

+ 109 - 118
src/views/import.vue → src/views/table/import.vue

@@ -1,118 +1,109 @@
-<template>
-    <div>
-        <div class="container">
-            <div class="handle-box">
-                <el-upload
-                    action="#"
-                    :limit="1"
-                    accept=".xlsx, .xls"
-                    :show-file-list="false"
-                    :before-upload="beforeUpload"
-                    :http-request="handleMany"
-                >
-                    <el-button class="mr10" type="success">批量导入</el-button>
-                </el-upload>
-                <el-link href="/template.xlsx" target="_blank">下载模板</el-link>
-            </div>
-            <el-table :data="tableData" border class="table" header-cell-class-name="table-header">
-                <el-table-column prop="id" label="ID" width="55" align="center"></el-table-column>
-                <el-table-column prop="name" label="姓名"></el-table-column>
-                <el-table-column prop="sno" label="学号"></el-table-column>
-                <el-table-column prop="class" label="班级"></el-table-column>
-                <el-table-column prop="age" label="年龄"></el-table-column>
-                <el-table-column prop="sex" label="性别"></el-table-column>
-            </el-table>
-        </div>
-    </div>
-</template>
-
-<script setup lang="ts" name="import">
-import { UploadProps } from 'element-plus';
-import { ref, reactive } from 'vue';
-import * as XLSX from 'xlsx';
-
-interface TableItem {
-    id: number;
-    name: string;
-    sno: string;
-    class: string;
-    age: string;
-    sex: string;
-}
-
-const tableData = ref<TableItem[]>([]);
-// 获取表格数据
-const getData = () => {
-    tableData.value = [
-        {
-            id: 1,
-            name: '小明',
-            sno: 'S001',
-            class: '一班',
-            age: '10',
-            sex: '男',
-        },
-        {
-            id: 2,
-            name: '小红',
-            sno: 'S002',
-            class: '一班',
-            age: '9',
-            sex: '女',
-        },
-    ];
-};
-getData();
-
-const importList = ref<any>([]);
-const beforeUpload: UploadProps['beforeUpload'] = async (rawFile) => {
-    importList.value = await analysisExcel(rawFile);
-    return true;
-};
-const analysisExcel = (file: any) => {
-    return new Promise(function (resolve, reject) {
-        const reader = new FileReader();
-        reader.onload = function (e: any) {
-            const data = e.target.result;
-            let datajson = XLSX.read(data, {
-                type: 'binary',
-            });
-
-            const sheetName = datajson.SheetNames[0];
-            const result = XLSX.utils.sheet_to_json(datajson.Sheets[sheetName]);
-            resolve(result);
-        };
-        reader.readAsBinaryString(file);
-    });
-};
-
-const handleMany = async () => {
-    // 把数据传给服务器后获取最新列表,这里只是示例,不做请求
-    const list = importList.value.map((item: any, index: number) => {
-        return {
-            id: index,
-            name: item['姓名'],
-            sno: item['学号'],
-            class: item['班级'],
-            age: item['年龄'],
-            sex: item['性别'],
-        };
-    });
-    tableData.value.push(...list);
-};
-</script>
-
-<style scoped>
-.handle-box {
-    display: flex;
-    margin-bottom: 20px;
-}
-
-.table {
-    width: 100%;
-    font-size: 14px;
-}
-.mr10 {
-    margin-right: 10px;
-}
-</style>
+<template>
+    <div>
+        <div class="container">
+            <div class="handle-box">
+                <el-upload action="#" :limit="1" accept=".xlsx, .xls" :show-file-list="false"
+                    :before-upload="beforeUpload" :http-request="handleMany">
+                    <el-button class="mr10" type="success">批量导入</el-button>
+                </el-upload>
+                <el-link href="/template.xlsx" target="_blank">下载模板</el-link>
+            </div>
+            <el-table :data="tableData" border class="table" header-cell-class-name="table-header">
+                <el-table-column prop="id" label="ID" width="55" align="center"></el-table-column>
+                <el-table-column prop="name" label="姓名"></el-table-column>
+                <el-table-column prop="sno" label="学号"></el-table-column>
+                <el-table-column prop="class" label="班级"></el-table-column>
+                <el-table-column prop="age" label="年龄"></el-table-column>
+                <el-table-column prop="sex" label="性别"></el-table-column>
+            </el-table>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts" name="import">
+import { UploadProps } from 'element-plus';
+import { ref, reactive } from 'vue';
+import * as XLSX from 'xlsx';
+
+interface TableItem {
+    id: number;
+    name: string;
+    sno: string;
+    class: string;
+    age: string;
+    sex: string;
+}
+
+const tableData = ref<TableItem[]>([]);
+// 获取表格数据
+const getData = () => {
+    tableData.value = [
+        {
+            id: 1,
+            name: '小明',
+            sno: 'S001',
+            class: '一班',
+            age: '10',
+            sex: '男',
+        },
+        {
+            id: 2,
+            name: '小红',
+            sno: 'S002',
+            class: '一班',
+            age: '9',
+            sex: '女',
+        },
+    ];
+};
+getData();
+
+const importList = ref<any>([]);
+const beforeUpload: UploadProps['beforeUpload'] = async (rawFile) => {
+    importList.value = await analysisExcel(rawFile);
+    return true;
+};
+const analysisExcel = (file: any) => {
+    return new Promise(function (resolve, reject) {
+        const reader = new FileReader();
+        reader.onload = function (e: any) {
+            const data = e.target.result;
+            let datajson = XLSX.read(data, {
+                type: 'binary',
+            });
+
+            const sheetName = datajson.SheetNames[0];
+            const result = XLSX.utils.sheet_to_json(datajson.Sheets[sheetName]);
+            resolve(result);
+        };
+        reader.readAsBinaryString(file);
+    });
+};
+
+const handleMany = async () => {
+    // 把数据传给服务器后获取最新列表,这里只是示例,不做请求
+    const list = importList.value.map((item: any, index: number) => {
+        return {
+            id: index,
+            name: item['姓名'],
+            sno: item['学号'],
+            class: item['班级'],
+            age: item['年龄'],
+            sex: item['性别'],
+        };
+    });
+    tableData.value.push(...list);
+};
+</script>
+
+<style scoped>
+.handle-box {
+    display: flex;
+    margin-bottom: 20px;
+}
+
+.table {
+    width: 100%;
+    font-size: 14px;
+}
+</style>

+ 79 - 0
src/views/table/table-editor.vue

@@ -0,0 +1,79 @@
+<template>
+	<div class="container">
+		<TableCustom :columns="columns" :tableData="tableData" :hasToolbar="false" :hasPagination="false">
+			<template #name="{ rows }">
+				<el-input v-if="rows.editing" v-model="rows.name"></el-input>
+				<span v-else>{{ rows.name }}</span>
+			</template>
+			<template #password="{ rows }">
+				<el-input v-if="rows.editing" v-model="rows.password"></el-input>
+				<span v-else>{{ rows.password }}</span>
+			</template>
+			<template #email="{ rows }">
+				<el-input v-if="rows.editing" v-model="rows.email"></el-input>
+				<span v-else>{{ rows.email }}</span>
+			</template>
+			<template #role="{ rows }">
+				<el-select v-if="rows.editing" v-model="rows.role">
+					<el-option label="管理员" value="管理员"></el-option>
+					<el-option label="普通用户" value="普通用户"></el-option>
+				</el-select>
+				<span v-else>{{ rows.role }}</span>
+			</template>
+			<template #operator="{ rows, index }">
+				<template v-if="!rows.editing">
+					<el-button type="primary" size="small" :icon="Edit" @click="handleEdit(rows)">
+						编辑
+					</el-button>
+					<el-button type="danger" size="small" :icon="Delete" @click="">
+						删除
+					</el-button>
+				</template>
+				<template v-else>
+					<el-button type="success" size="small" :icon="Select" @click="rows.editing = false">
+						保存
+					</el-button>
+					<el-button type="default" size="small" :icon="CloseBold" @click="handleCancel(rows, index)">
+						取消
+					</el-button>
+				</template>
+			</template>
+		</TableCustom>
+	</div>
+</template>
+
+<script setup lang="ts" name="table-editor">
+import { ref } from 'vue';
+import { Delete, Edit, CloseBold, Select } from '@element-plus/icons-vue';
+import TableCustom from '@/components/table-custom.vue';
+import { fetchUserData } from '@/api/index';
+
+let columns = ref([
+	{ type: 'index', label: '序号', width: 55, align: 'center' },
+	{ prop: 'name', label: '用户名' },
+	{ prop: 'password', label: '密码' },
+	{ prop: 'email', label: '邮箱' },
+	{ prop: 'role', label: '角色' },
+	{ prop: 'operator', label: '操作', width: 180 },
+])
+const tableData = ref([]);
+const getData = async () => {
+	const res = await fetchUserData();
+	tableData.value = res.data.list;
+};
+getData();
+
+const rowData = ref({})
+
+const handleEdit = (row) => {
+	rowData.value = { ...row };
+	row.editing = true;
+};
+
+const handleCancel = (row, index) => {
+	row.editing = false;
+	tableData.value[index] = { ...rowData.value };
+};
+</script>
+
+<style scoped></style>

+ 0 - 116
src/views/tabs.vue

@@ -1,116 +0,0 @@
-<template>
-	<div class="container">
-		<el-tabs v-model="message">
-			<el-tab-pane :label="`未读消息(${state.unread.length})`" name="first">
-				<el-table :data="state.unread" :show-header="false" style="width: 100%">
-					<el-table-column>
-						<template #default="scope">
-							<span class="message-title">{{ scope.row.title }}</span>
-						</template>
-					</el-table-column>
-					<el-table-column prop="date" width="180"></el-table-column>
-					<el-table-column width="120">
-						<template #default="scope">
-							<el-button size="small" @click="handleRead(scope.$index)">标为已读</el-button>
-						</template>
-					</el-table-column>
-				</el-table>
-				<div class="handle-row">
-					<el-button type="primary">全部标为已读</el-button>
-				</div>
-			</el-tab-pane>
-			<el-tab-pane :label="`已读消息(${state.read.length})`" name="second">
-				<template v-if="message === 'second'">
-					<el-table :data="state.read" :show-header="false" style="width: 100%">
-						<el-table-column>
-							<template #default="scope">
-								<span class="message-title">{{ scope.row.title }}</span>
-							</template>
-						</el-table-column>
-						<el-table-column prop="date" width="180"></el-table-column>
-						<el-table-column width="120">
-							<template #default="scope">
-								<el-button type="danger" size="small" @click="handleDel(scope.$index)">删除</el-button>
-							</template>
-						</el-table-column>
-					</el-table>
-					<div class="handle-row">
-						<el-button type="danger">删除全部</el-button>
-					</div>
-				</template>
-			</el-tab-pane>
-			<el-tab-pane :label="`回收站(${state.recycle.length})`" name="third">
-				<template v-if="message === 'third'">
-					<el-table :data="state.recycle" :show-header="false" style="width: 100%">
-						<el-table-column>
-							<template #default="scope">
-								<span class="message-title">{{ scope.row.title }}</span>
-							</template>
-						</el-table-column>
-						<el-table-column prop="date" width="180"></el-table-column>
-						<el-table-column width="120">
-							<template #default="scope">
-								<el-button size="small" @click="handleRestore(scope.$index)">还原</el-button>
-							</template>
-						</el-table-column>
-					</el-table>
-					<div class="handle-row">
-						<el-button type="danger">清空回收站</el-button>
-					</div>
-				</template>
-			</el-tab-pane>
-		</el-tabs>
-	</div>
-</template>
-
-<script setup lang="ts" name="tabs">
-import { ref, reactive } from 'vue';
-
-const message = ref('first');
-const state = reactive({
-	unread: [
-		{
-			date: '2018-04-19 20:00:00',
-			title: '【系统通知】该系统将于今晚凌晨2点到5点进行升级维护'
-		},
-		{
-			date: '2018-04-19 21:00:00',
-			title: '今晚12点整发大红包,先到先得'
-		}
-	],
-	read: [
-		{
-			date: '2018-04-19 20:00:00',
-			title: '【系统通知】该系统将于今晚凌晨2点到5点进行升级维护'
-		}
-	],
-	recycle: [
-		{
-			date: '2018-04-19 20:00:00',
-			title: '【系统通知】该系统将于今晚凌晨2点到5点进行升级维护'
-		}
-	]
-});
-
-const handleRead = (index: number) => {
-	const item = state.unread.splice(index, 1);
-	state.read = item.concat(state.read);
-};
-const handleDel = (index: number) => {
-	const item = state.read.splice(index, 1);
-	state.recycle = item.concat(state.recycle);
-};
-const handleRestore = (index: number) => {
-	const item = state.recycle.splice(index, 1);
-	state.read = item.concat(state.read);
-};
-</script>
-
-<style>
-.message-title {
-	cursor: pointer;
-}
-.handle-row {
-	margin-top: 30px;
-}
-</style>

+ 0 - 174
src/views/user.vue

@@ -1,174 +0,0 @@
-<template>
-	<div>
-		<el-row :gutter="20">
-			<el-col :span="12">
-				<el-card shadow="hover">
-					<template #header>
-						<div class="clearfix">
-							<span>基础信息</span>
-						</div>
-					</template>
-					<div class="info">
-						<div class="info-image" @click="showDialog">
-							<el-avatar :size="100" :src="avatarImg" />
-							<span class="info-edit">
-								<i class="el-icon-lx-camerafill"></i>
-							</span>
-						</div>
-						<div class="info-name">{{ name }}</div>
-						<div class="info-desc">不可能!我的代码怎么可能会有bug!</div>
-					</div>
-				</el-card>
-			</el-col>
-			<el-col :span="12">
-				<el-card shadow="hover">
-					<template #header>
-						<div class="clearfix">
-							<span>账户编辑</span>
-						</div>
-					</template>
-					<el-form label-width="90px">
-						<el-form-item label="用户名:"> {{ name }} </el-form-item>
-						<el-form-item label="旧密码:">
-							<el-input type="password" v-model="form.old"></el-input>
-						</el-form-item>
-						<el-form-item label="新密码:">
-							<el-input type="password" v-model="form.new"></el-input>
-						</el-form-item>
-						<el-form-item label="个人简介:">
-							<el-input v-model="form.desc"></el-input>
-						</el-form-item>
-						<el-form-item>
-							<el-button type="primary" @click="onSubmit">保存</el-button>
-						</el-form-item>
-					</el-form>
-				</el-card>
-			</el-col>
-		</el-row>
-		<el-dialog title="裁剪图片" v-model="dialogVisible" width="600px">
-			<vue-cropper
-				ref="cropper"
-				:src="imgSrc"
-				:ready="cropImage"
-				:zoom="cropImage"
-				:cropmove="cropImage"
-				style="width: 100%; height: 400px"
-			></vue-cropper>
-
-			<template #footer>
-				<span class="dialog-footer">
-					<el-button class="crop-demo-btn" type="primary"
-						>选择图片
-						<input class="crop-input" type="file" name="image" accept="image/*" @change="setImage" />
-					</el-button>
-					<el-button type="primary" @click="saveAvatar">上传并保存</el-button>
-				</span>
-			</template>
-		</el-dialog>
-	</div>
-</template>
-
-<script setup lang="ts" name="user">
-import { reactive, ref } from 'vue';
-import VueCropper from 'vue-cropperjs';
-import 'cropperjs/dist/cropper.css';
-import avatar from '../assets/img/img.jpg';
-
-const name = localStorage.getItem('ms_username');
-const form = reactive({
-	old: '',
-	new: '',
-	desc: '不可能!我的代码怎么可能会有bug!'
-});
-const onSubmit = () => {};
-
-const avatarImg = ref(avatar);
-const imgSrc = ref('');
-const cropImg = ref('');
-const dialogVisible = ref(false);
-const cropper: any = ref();
-
-const showDialog = () => {
-	dialogVisible.value = true;
-	imgSrc.value = avatarImg.value;
-};
-
-const setImage = (e: any) => {
-	const file = e.target.files[0];
-	if (!file.type.includes('image/')) {
-		return;
-	}
-	const reader = new FileReader();
-	reader.onload = (event: any) => {
-		dialogVisible.value = true;
-		imgSrc.value = event.target.result;
-		cropper.value && cropper.value.replace(event.target.result);
-	};
-	reader.readAsDataURL(file);
-};
-
-const cropImage = () => {
-	cropImg.value = cropper.value.getCroppedCanvas().toDataURL();
-};
-
-const saveAvatar = () => {
-	avatarImg.value = cropImg.value;
-	dialogVisible.value = false;
-};
-</script>
-
-<style scoped>
-.info {
-	text-align: center;
-	padding: 35px 0;
-}
-.info-image {
-	position: relative;
-	margin: auto;
-	width: 100px;
-	height: 100px;
-	background: #f8f8f8;
-	border: 1px solid #eee;
-	border-radius: 50px;
-	overflow: hidden;
-}
-
-.info-edit {
-	display: flex;
-	justify-content: center;
-	align-items: center;
-	position: absolute;
-	left: 0;
-	top: 0;
-	width: 100%;
-	height: 100%;
-	background: rgba(0, 0, 0, 0.5);
-	opacity: 0;
-	transition: opacity 0.3s ease;
-}
-.info-edit i {
-	color: #eee;
-	font-size: 25px;
-}
-.info-image:hover .info-edit {
-	opacity: 1;
-}
-.info-name {
-	margin: 15px 0 10px;
-	font-size: 24px;
-	font-weight: 500;
-	color: #262626;
-}
-.crop-demo-btn {
-	position: relative;
-}
-.crop-input {
-	position: absolute;
-	width: 100px;
-	height: 40px;
-	left: 0;
-	top: 0;
-	opacity: 0;
-	cursor: pointer;
-}
-</style>

+ 10 - 10
src/vite-env.d.ts

@@ -1,10 +1,10 @@
-/// <reference types="vite/client" />
-
-declare module '*.vue' {
-  import type { DefineComponent } from 'vue'
-  const component: DefineComponent<{}, {}, any>
-  export default component
-}
-
-declare module 'vue-schart';
-declare module 'vue-cropperjs';
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}
+
+declare module 'vue-schart';
+declare module 'nprogress'

+ 22 - 18
tsconfig.json

@@ -1,18 +1,22 @@
-{
-  "compilerOptions": {
-    "target": "ESNext",
-    "useDefineForClassFields": true,
-    "module": "ESNext",
-    "moduleResolution": "Node",
-    "strict": false,
-    "jsx": "preserve",
-    "sourceMap": true,
-    "resolveJsonModule": true,
-    "isolatedModules": true,
-    "esModuleInterop": true,
-    "lib": ["ESNext", "DOM"],
-    "skipLibCheck": true
-  },
-  "include": ["src/**/*.ts", "src/**/*.d.ts","src/**/*.vue"],
-  "references": [{ "path": "./tsconfig.node.json" }]
-}
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "strict": false,
+    "jsx": "preserve",
+    "sourceMap": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "esModuleInterop": true,
+    "lib": ["ESNext", "DOM"],
+    "skipLibCheck": true,
+    "baseUrl": "./",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "include": ["src/**/*.ts", "src/**/*.d.ts","src/**/*.vue"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 9 - 9
tsconfig.node.json

@@ -1,9 +1,9 @@
-{
-  "compilerOptions": {
-    "composite": true,
-    "module": "ESNext",
-    "moduleResolution": "Node",
-    "allowSyntheticDefaultImports": true
-  },
-  "include": ["vite.config.ts"]
-}
+{
+  "compilerOptions": {
+    "composite": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 31 - 22
vite.config.ts

@@ -1,22 +1,31 @@
-import { defineConfig } from 'vite';
-import vue from '@vitejs/plugin-vue';
-import VueSetupExtend from 'vite-plugin-vue-setup-extend';
-import AutoImport from 'unplugin-auto-import/vite';
-import Components from 'unplugin-vue-components/vite';
-import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
-export default defineConfig({
-	base: './',
-	plugins: [
-		vue(),
-		VueSetupExtend(),
-		AutoImport({
-			resolvers: [ElementPlusResolver()]
-		}),
-		Components({
-			resolvers: [ElementPlusResolver()]
-		})
-	],
-	optimizeDeps: {
-		include: ['schart.js']
-	}
-});
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import VueSetupExtend from 'vite-plugin-vue-setup-extend';
+import AutoImport from 'unplugin-auto-import/vite';
+import Components from 'unplugin-vue-components/vite';
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
+export default defineConfig({
+	base: './',
+	plugins: [
+		vue(),
+		VueSetupExtend(),
+		AutoImport({
+			resolvers: [ElementPlusResolver()]
+		}),
+		Components({
+			resolvers: [ElementPlusResolver()]
+		})
+	],
+	optimizeDeps: {
+		include: ['schart.js']
+	},
+	resolve: {
+		alias: {
+			'@': '/src',
+			'~': '/src/assets'
+		}
+	},
+	define: {
+		__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "true",
+	},
+});

File diff suppressed because it is too large
+ 252 - 295
yarn.lock


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