Bladeren bron

first commit

ith5 6 maanden geleden
commit
6be8e98ea7
100 gewijzigde bestanden met toevoegingen van 6305 en 0 verwijderingen
  1. 5 0
      .env
  2. 7 0
      .env.development
  3. 7 0
      .env.production
  4. 6 0
      .gitignore
  5. 21 0
      LICENSE
  6. 49 0
      README.md
  7. 166 0
      index.html
  8. 12 0
      jsconfig.json
  9. 54 0
      package.json
  10. 6 0
      postcss.config.cjs
  11. 69 0
      public/auth-bg.svg
  12. BIN
      public/avatar.jpg
  13. BIN
      public/favicon.ico
  14. BIN
      public/logo.png
  15. BIN
      public/not-image.png
  16. BIN
      public/skins-thumb/businessGray/thumb.jpg
  17. BIN
      public/skins-thumb/city/thumb.jpg
  18. BIN
      public/skins-thumb/classics/thumb.jpg
  19. BIN
      public/skins-thumb/mine/thumb.jpg
  20. 15 0
      src/App.vue
  21. 18 0
      src/api/center/common.js
  22. 41 0
      src/api/center/game.js
  23. 206 0
      src/api/common.js
  24. 54 0
      src/api/login.js
  25. 27 0
      src/api/system/attachment.js
  26. 138 0
      src/api/system/config.js
  27. 99 0
      src/api/system/database.js
  28. 121 0
      src/api/system/dept.js
  29. 137 0
      src/api/system/dict.js
  30. 30 0
      src/api/system/emailLog.js
  31. 30 0
      src/api/system/loginLog.js
  32. 63 0
      src/api/system/menu.js
  33. 17 0
      src/api/system/monitor.js
  34. 77 0
      src/api/system/notice.js
  35. 30 0
      src/api/system/operLog.js
  36. 77 0
      src/api/system/post.js
  37. 109 0
      src/api/system/role.js
  38. 146 0
      src/api/system/user.js
  39. 113 0
      src/api/tool/crontab.js
  40. 135 0
      src/api/tool/generate.js
  41. 176 0
      src/assets/404.svg
  42. 0 0
      src/assets/image/action.svg
  43. 1 0
      src/assets/image/attach.svg
  44. 1 0
      src/assets/image/login.svg
  45. 0 0
      src/assets/image/user.svg
  46. 0 0
      src/assets/login_picture.svg
  47. BIN
      src/assets/userBanner.jpg
  48. 130 0
      src/components/game-permission/index.vue
  49. 74 0
      src/components/index.js
  50. 179 0
      src/components/ma-cityLinkage/index.vue
  51. 0 0
      src/components/ma-cityLinkage/lib/city.json
  52. 108 0
      src/components/ma-codeEditor/index.vue
  53. 77 0
      src/components/ma-colorPicker/index.vue
  54. 110 0
      src/components/ma-verifyCode/index.vue
  55. 194 0
      src/components/ma-wangEditor/index.vue
  56. 38 0
      src/components/sa-chart/index.vue
  57. 41 0
      src/components/sa-checkbox/index.vue
  58. 62 0
      src/components/sa-dict/index.vue
  59. 0 0
      src/components/sa-icon-picker/iconify/bi.json
  60. 121 0
      src/components/sa-icon-picker/index.vue
  61. 29 0
      src/components/sa-icon/index.vue
  62. 55 0
      src/components/sa-radio/index.vue
  63. 82 0
      src/components/sa-resource/button.vue
  64. 253 0
      src/components/sa-resource/index.vue
  65. 55 0
      src/components/sa-select/index.vue
  66. 60 0
      src/components/sa-switch/index.vue
  67. 130 0
      src/components/sa-table/defaultOptions.js
  68. 72 0
      src/components/sa-table/import.vue
  69. 768 0
      src/components/sa-table/index.vue
  70. 120 0
      src/components/sa-treeSlider/index.vue
  71. 206 0
      src/components/sa-upload-file/index.vue
  72. 221 0
      src/components/sa-upload-image/index.vue
  73. 159 0
      src/components/sa-user/index.vue
  74. 18 0
      src/config/skins.js
  75. 8 0
      src/directives/auth/auth.js
  76. 29 0
      src/directives/auth/index.js
  77. 27 0
      src/directives/copy/index.js
  78. 12 0
      src/directives/index.js
  79. 31 0
      src/directives/role/index.js
  80. 8 0
      src/directives/role/role.js
  81. 3 0
      src/i18n/en/crud.js
  82. 8 0
      src/i18n/en/maResource.js
  83. 53 0
      src/i18n/en/menus.js
  84. 14 0
      src/i18n/en/skin.js
  85. 92 0
      src/i18n/en/sys.js
  86. 8 0
      src/i18n/en/upload.js
  87. 0 0
      src/i18n/en/user.js
  88. 50 0
      src/i18n/index.js
  89. 3 0
      src/i18n/zh_CN/crud.js
  90. 8 0
      src/i18n/zh_CN/maResource.js
  91. 53 0
      src/i18n/zh_CN/menus.js
  92. 14 0
      src/i18n/zh_CN/skin.js
  93. 92 0
      src/i18n/zh_CN/sys.js
  94. 8 0
      src/i18n/zh_CN/upload.js
  95. 4 0
      src/i18n/zh_CN/user.js
  96. 19 0
      src/layout/404.vue
  97. 62 0
      src/layout/components/banner/index.vue
  98. 21 0
      src/layout/components/classic/index.vue
  99. 16 0
      src/layout/components/classic/ma-classic-header.vue
  100. 37 0
      src/layout/components/classic/ma-classic-slider.vue

+ 5 - 0
.env

@@ -0,0 +1,5 @@
+VITE_APP_TITLE = 筷子科技
+VITE_APP_PORT = 8888
+VITE_APP_OPEN_PROXY = true
+VITE_APP_BASE = /
+VITE_APP_TOKEN_PREFIX = token

+ 7 - 0
.env.development

@@ -0,0 +1,7 @@
+# .env.development
+VITE_APP_ENV = development
+
+VITE_APP_BASE_URL = http://127.0.0.1:8787
+VITE_APP_WS_URL = ws://127.0.0.1:3131
+VITE_APP_WS_APPKEY = 8c849eaf1e166c0a71d02fcec7c8df78
+VITE_APP_PROXY_PREFIX = /dev

+ 7 - 0
.env.production

@@ -0,0 +1,7 @@
+# .env.production
+VITE_APP_ENV = production
+
+VITE_APP_BASE_URL = http://admin.saithink.top
+VITE_APP_WS_URL = ws://127.0.0.1:3131
+VITE_APP_WS_APPKEY = 2f97f2e18b6b6e6304ce77fdb779c650
+VITE_APP_PROXY_PREFIX = /prod

+ 6 - 0
.gitignore

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

+ 21 - 0
LICENSE

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

+ 49 - 0
README.md

@@ -0,0 +1,49 @@
+<p align="center">
+  <img src="https://saithink.top/images/logo.png" width="120" />
+</p>
+<p align="center">
+  <img src="https://svg.hamm.cn/badge.svg?key=License&value=MIT" />
+  <img src="https://svg.hamm.cn/badge.svg?key=Version&value=5.x" />
+</p>
+
+## 简介
+
+`Saidmin Vue` 5.x
+
+## 安装使用
+
+- 获取代码
+
+```
+git clone https://github.com/saithink/saiadmin-vue.git
+```
+
+或
+
+```
+git clone https://gitee.com/appsai/saiadmin-vue.git
+```
+
+或
+
+```
+git clone https://gitcode.com/saigroup/saiadmin-vue.git
+```
+
+- 安装依赖
+
+```
+cd saiadmin-vue && yarn install
+```
+
+- 运行
+
+```
+yarn dev
+```
+
+- 打包
+
+```
+yarn build
+```

+ 166 - 0
index.html

@@ -0,0 +1,166 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>%VITE_APP_TITLE%</title>
+		<meta charset="UTF-8" />
+		<meta name="renderer" content="webkit" />
+		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
+		<style>
+			html,
+			body,
+			#app {
+				height: 100%;
+			}
+
+			* {
+				margin: 0;
+				padding: 0;
+				font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
+			}
+
+			.preload__wrap {
+				display: flex;
+				flex-direction: column;
+				letter-spacing: 1px;
+				background-color: #2f3447;
+				position: fixed;
+				left: 0;
+				top: 0;
+				height: 100%;
+				width: 100%;
+				z-index: 9999;
+				transition: all 0.3s ease-in;
+				opacity: 1;
+				pointer-events: none;
+			}
+
+			.preload__wrap.is-hide {
+				opacity: 0;
+			}
+
+			.preload__container {
+				display: flex;
+				justify-content: center;
+				align-items: center;
+				flex-direction: column;
+				width: 100%;
+				user-select: none;
+				-webkit-user-select: none;
+				flex-grow: 1;
+			}
+
+			.preload__name {
+				font-size: 30px;
+				color: #fff;
+				letter-spacing: 5px;
+				font-weight: bold;
+				margin-bottom: 30px;
+			}
+
+			.preload__title {
+				color: #fff;
+				font-size: 14px;
+				margin: 30px 0 20px 0;
+			}
+
+			.preload__sub-title {
+				color: #ababab;
+				font-size: 12px;
+			}
+
+			.preload__footer {
+				text-align: center;
+				padding: 10px 0 20px 0;
+			}
+
+			.preload__footer a {
+				font-size: 12px;
+				color: #ababab;
+				text-decoration: none;
+			}
+
+			.preload__loading {
+				height: 30px;
+				width: 30px;
+				border-radius: 30px;
+				border: 7px solid currentColor;
+				border-bottom-color: #2f3447 !important;
+				position: relative;
+				animation: r 1s infinite cubic-bezier(0.17, 0.67, 0.83, 0.67), bc 2s infinite ease-in;
+				transform: rotate(0deg);
+			}
+
+			@keyframes r {
+				from {
+					transform: rotate(0deg);
+				}
+
+				to {
+					transform: rotate(360deg);
+				}
+			}
+
+			.preload__loading::after,
+			.preload__loading::before {
+				content: '';
+				display: inline-block;
+				position: absolute;
+				bottom: -2px;
+				height: 7px;
+				width: 7px;
+				border-radius: 10px;
+				background-color: currentColor;
+			}
+
+			.preload__loading::after {
+				left: -1px;
+			}
+
+			.preload__loading::before {
+				right: -1px;
+			}
+
+			@keyframes bc {
+				0% {
+					color: #689cc5;
+				}
+
+				25% {
+					color: #b3b7e2;
+				}
+
+				50% {
+					color: #93dbe9;
+				}
+
+				75% {
+					color: #abbd81;
+				}
+
+				100% {
+					color: #689cc5;
+				}
+			}
+		</style>
+	</head>
+	<body>
+		<noscript>
+			<strong>你的浏览器未开启javascript支持,请开启后刷新页面访问!</strong>
+		</noscript>
+		<div id="app" class="ma-ui">
+			<div class="preload__wrap" id="Loading">
+				<div class="preload__container">
+					<p class="preload__name">%VITE_APP_TITLE%</p>
+					<div class="preload__loading"></div>
+					<p class="preload__title">正在加载资源...</p>
+					<p class="preload__sub-title">初次加载资源可能需要较多时间 请耐心等待</p>
+				</div>
+				<div class="preload__footer">
+					<a href="https://saithink.top" target="_blank">https://saithink.top</a>
+				</div>
+			</div>
+		</div>
+		<script type="module" src="/src/main.js"></script>
+	</body>
+</html>

+ 12 - 0
jsconfig.json

@@ -0,0 +1,12 @@
+{
+    "compilerOptions": {
+        "baseUrl": ".",
+        "paths": {
+            "@/*": ["src/*"],
+            "@cps/*": ["src/components/*"],
+            "vue-i18n": ["vue-i18n/dist/vue-i18n.cjs.js"]
+        },
+        "jsx": "preserve"
+    },
+    "exclude": ["node_modules", "dist", "build"]
+}

+ 54 - 0
package.json

@@ -0,0 +1,54 @@
+{
+  "name": "saiadmin-vue",
+  "admin_name": "saiadmin",
+  "version": "5.0",
+  "type": "module",
+  "license": "MIT",
+  "scripts": {
+    "dev": "vite serve --mode development",
+    "build": "vite build",
+    "preview": "vite preview",
+    "tailwind": "tailwind-config-viewer -o -c tailwind.config.cjs"
+  },
+  "dependencies": {
+    "@arco-design/color": "^0.4.0",
+    "@arco-design/web-vue": "^2.57.0",
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "^5.1.12",
+    "autoprefixer": "^10.4.17",
+    "axios": "^0.27.2",
+    "crypto-js": "^4.2.0",
+    "dayjs": "^1.11.11",
+    "echarts": "^5.4.2",
+    "file2md5": "^1.3.0",
+    "lodash": "^4.17.21",
+    "md-editor-v3": "^4.13.5",
+    "monaco-editor": "^0.33.0",
+    "nprogress": "^0.2.0",
+    "pinia": "^2.1.7",
+    "postcss-import": "^14.1.0",
+    "qs": "^6.10.3",
+    "resize-observer-polyfill": "^1.5.1",
+    "sortablejs": "^1.15.0",
+    "tailwindcss": "^3.4.1",
+    "vue": "^3.4.19",
+    "vue-clipboard3": "^2.0.0",
+    "vue-color-kit": "^1.0.5",
+    "vue-echarts": "^6.0.2",
+    "vue-i18n": "^9.1.10",
+    "vue-router": "^4.2.5",
+    "vuedraggable": "^4.1.0"
+  },
+  "devDependencies": {
+    "@iconify/vue": "^4.2.0",
+    "@vitejs/plugin-vue": "^5.0.4",
+    "@vitejs/plugin-vue-jsx": "^3.1.0",
+    "less": "^4.1.3",
+    "less-loader": "^11.1.4",
+    "rollup-plugin-visualizer": "^5.12.0",
+    "tailwind-config-viewer": "^1.7.3",
+    "typescript": "^4.7.4",
+    "vite": "^5.1.4"
+  },
+  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
+}

+ 6 - 0
postcss.config.cjs

@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+}

+ 69 - 0
public/auth-bg.svg

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+    <title>Group 21</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
+            <g id="Group-21" transform="translate(77.000000, 73.000000)">
+                <g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
+                    <ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
+                    <ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
+                    <path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
+                    <path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
+                    <path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
+                    <path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
+                    <g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
+                        <ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
+                        <path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
+                    </g>
+                </g>
+                <g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
+                    <ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
+                    <ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
+                    <ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
+                    <ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
+                    <path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
+                    <g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
+                        <ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
+                        <path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
+                    </g>
+                    <ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
+                    <ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
+                    <ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
+                    <path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
+                </g>
+                <g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
+                    <ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
+                    <g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
+                        <ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
+                        <path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
+                    </g>
+                    <path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
+                    <ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
+                    <ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
+                    <path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
+                </g>
+                <g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
+                    <g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
+                        <circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
+                        <path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
+                    </g>
+                    <circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
+                    <path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
+                    <path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
+                    <polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
+                    <path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
+                    <path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
+                    <path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
+                    <circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
+                    <circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
+                    <circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
+                    <circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
+                    <circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
public/avatar.jpg


BIN
public/favicon.ico


BIN
public/logo.png


BIN
public/not-image.png


BIN
public/skins-thumb/businessGray/thumb.jpg


BIN
public/skins-thumb/city/thumb.jpg


BIN
public/skins-thumb/classics/thumb.jpg


BIN
public/skins-thumb/mine/thumb.jpg


+ 15 - 0
src/App.vue

@@ -0,0 +1,15 @@
+<script setup>
+  import cn from '@arco-design/web-vue/es/locale/lang/zh-cn'
+  import en from '@arco-design/web-vue/es/locale/lang/en-us'
+  import { ref } from 'vue'
+  import { useAppStore } from './store'
+  const appStore = useAppStore()
+  const lang = ref(appStore.language === 'zh_CN' ? cn : en)
+
+</script>
+
+<template>
+  <a-config-provider :locale="lang" :update-at-scroll="true">
+    <router-view />
+  </a-config-provider>
+</template>

+ 18 - 0
src/api/center/common.js

@@ -0,0 +1,18 @@
+import { request } from "@/utils/request.js";
+
+/**
+ * Center通用接口
+ */
+export default {
+  /**
+   * 获取游戏权限
+   * @returns
+   */
+  getMainGameOptionsApi(params = {}) {
+    return request({
+      url: "v1/center/common/getMainGameOptions",
+      method: "get",
+      params,
+    });
+  },
+};

+ 41 - 0
src/api/center/game.js

@@ -0,0 +1,41 @@
+import { request } from "@/utils/request.js";
+
+/**
+ * Center游戏接口
+ */
+export default {
+  /**
+   * 根据部门ID获取游戏IDS
+   * @returns
+   */
+  getGameListByDeptId(params = {}) {
+    return request({
+      url: "/v1/center/Game/getGameListByDeptId",
+      method: "get",
+      params,
+    });
+  },
+
+  /**
+   * 获取所有的游戏数据
+   * @returns
+   */
+  getAllGameData() {
+    return request({
+      url: "/v1/center/Game/getAllGameData",
+      method: "get",
+    });
+  },
+
+  /**
+   * 设置部门游戏权限
+   * @returns
+   */
+  setGameListByDeptId(data = {}) {
+    return request({
+      url: "/v1/center/Game/setGameListByDeptId",
+      method: "post",
+      data,
+    });
+  },
+};

+ 206 - 0
src/api/common.js

@@ -0,0 +1,206 @@
+import { request } from '@/utils/request.js'
+
+export default {
+  /**
+   * 获取用户列表
+   * @returns
+   */
+  getUserList(params = {}) {
+    return request({
+      url: '/core/system/getUserList',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 通过id 列表获取用户基础信息
+   * @returns
+   */
+  getUserInfoByIds(data = {}) {
+    return request({
+      url: '/core/system/getUserInfoByIds',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 获取公告列表
+   * @returns
+   */
+  getNoticeList(params = {}) {
+    return request({
+      url: '/core/system/notice',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 获取基础统计
+   * @returns
+   */
+  getStatistics(params = {}) {
+    return request({
+      url: '/core/system/statistics',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 获取登录图表统计
+   * @returns
+   */
+  loginChart(params = {}) {
+    return request({
+      url: '/core/system/loginChart',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 清除所有缓存
+   * @returns
+   */
+  clearAllCache() {
+    return request({
+      url: '/core/system/clearAllCache',
+      method: 'get'
+    })
+  },
+
+  /**
+   * 上传图片接口
+   * @returns
+   */
+  uploadImage(data = {}) {
+    return request({
+      url: '/core/system/uploadImage',
+      method: 'post',
+      timeout: 30000,
+      // headers: { 'Content-Type': 'multipart/form-data' },
+      data
+    })
+  },
+
+  /**
+   * 上传文件接口
+   * @returns
+   */
+  uploadFile(data = {}) {
+    return request({
+      url: '/core/system/uploadFile',
+      method: 'post',
+      timeout: 30000,
+      // headers: { 'Content-Type': 'multipart/form-data' },
+      data
+    })
+  },
+
+  /**
+   * 保存网络图片
+   * @returns
+   */
+  saveNetWorkImage(data = {}) {
+    return request({
+      url: '/core/system/saveNetworkImage',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 获取登录日志列表
+   */
+  getLoginLogList(params = {}) {
+    return request({
+      url: '/core/system/getLoginLogList',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 获取操作日志列表
+   */
+  getOperationLogList(params = {}) {
+    return request({
+      url: '/core/system/getOperationLogList',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 获取资源列表
+   */
+  getResourceList(params = {}) {
+    return request({
+      url: '/core/system/getResourceList',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 通用导入Excel
+   */
+  importExcel(url, data) {
+    return request({
+      url,
+      method: 'post',
+      data,
+      timeout: 30 * 1000
+      // headers: { 'Content-Type': 'multipart/form-data' },
+    })
+  },
+
+  /**
+   * 下载通用方法
+   */
+  download(url, method = 'post') {
+    return request({ url, method, responseType: 'blob' })
+  },
+
+  /**
+   * GET通用方法
+   */
+  commonGet(url, params = {}) {
+    return request({ url, method: 'get', params })
+  },
+
+  /**
+   * 查询所有字典数据
+   */
+  dictAll() {
+    return request({
+      url: '/core/system/dictAll',
+      method: 'get'
+    })
+  },
+
+  /**
+   * 根据id下载资源
+   */
+  downloadById(id) {
+    return request({
+      url: '/core/system/downloadById?id=' + id,
+      responseType: 'blob',
+      method: 'get'
+    })
+  },
+
+  /**
+   * 根据hash下载资源
+   */
+  downloadByHash(hash) {
+    return request({
+      url: '/core/system/downloadByHash?hash=' + hash,
+      responseType: 'blob',
+      method: 'get'
+    })
+  }
+}

+ 54 - 0
src/api/login.js

@@ -0,0 +1,54 @@
+import { request } from '@/utils/request.js'
+
+export default {
+	/**
+	 * 获取验证码
+	 * @returns
+	 */
+	getCaptch() {
+		// return import.meta.env.VITE_APP_PROXY_PREFIX + '/core/captcha?' + Date.parse(new Date().toString())
+		return request({
+			url: '/core/captcha',
+			method: 'get',
+		})
+	},
+
+	/**
+	 * 用户登录
+	 * @param {object} params
+	 * @returns
+	 */
+	login(params = {}) {
+		return request({
+			url: '/core/login',
+			method: 'post',
+			data: params,
+		})
+	},
+
+	/**
+	 * 用户退出
+	 * @param {object} params
+	 * @returns
+	 */
+	logout(params = {}) {
+		return request({
+			url: '/core/logout',
+			method: 'post',
+			data: params,
+		})
+	},
+
+	/**
+	 * 获取登录用户信息
+	 * @param {object} params
+	 * @returns
+	 */
+	getInfo(params = {}) {
+		return request({
+			url: '/core/system/user',
+			method: 'get',
+			data: params,
+		})
+	},
+}

+ 27 - 0
src/api/system/attachment.js

@@ -0,0 +1,27 @@
+import { request } from '@/utils/request.js'
+
+export default {
+  /**
+   * 获取文件分页列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/core/attachment/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/core/attachment/destroy',
+      method: 'delete',
+      data
+    })
+  }
+}

+ 138 - 0
src/api/system/config.js

@@ -0,0 +1,138 @@
+import { request } from '@/utils/request.js'
+
+/**
+ * 系统设置接口
+ */
+export default {
+  /**
+   * 获取配置列表
+   * @returns
+   */
+  getConfigList(params) {
+    return request({
+      url: '/core/config/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 删除配置
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/core/config/destroy',
+      method: 'delete',
+      data
+    })
+  },
+
+  /**
+   * 保存配置
+   * @returns
+   */
+  save(data = {}) {
+    return request({
+      url: '/core/config/save',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 修改配置
+   * @returns
+   */
+  update(id, data = {}) {
+    return request({
+      url: '/core/config/update?id=' + id,
+      method: 'put',
+      data
+    })
+  },
+
+  /**
+   * 按 keys 更新配置
+   * @returns
+   */
+  updateByKeys(data) {
+    return request({
+      url: '/core/config/updateByKeys',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 批量修改配置值
+   * @returns
+   */
+  batchUpdate(data) {
+    return request({
+      url: '/core/config/batchUpdate',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 获取组列表
+   * @returns
+   */
+  getConfigGroupList(params = {}) {
+    return request({
+      url: '/core/configGroup/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 保存配置组
+   * @returns
+   */
+  saveConfigGroup(data = {}) {
+    return request({
+      url: '/core/configGroup/save',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 更新配置组
+   * @returns
+   */
+  updateConfigGroup(id, data = {}) {
+    return request({
+      url: '/core/configGroup/update?id=' + id,
+      method: 'put',
+      data
+    })
+  },
+
+  /**
+   * 删除配置组
+   * @returns
+   */
+  deleteConfigGroup(data = {}) {
+    return request({
+      url: '/core/configGroup/destroy',
+      method: 'delete',
+      data
+    })
+  },
+
+  /**
+   * 邮箱测试
+   * @returns
+   */
+  testEmail(data = {}) {
+    return request({
+      url: '/core/configGroup/email',
+      method: 'post',
+      data
+    })
+  }
+}

+ 99 - 0
src/api/system/database.js

@@ -0,0 +1,99 @@
+import { request } from '@/utils/request.js'
+
+export default {
+  /**
+   * 获取数据表分页列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/core/database/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 获取数据源
+   * @returns
+   */
+  getDataSource(params = {}) {
+    return request({
+      url: '/core/database/dataSource',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 获取表字段列表
+   * @returns
+   */
+  getDetailed(params = {}) {
+    return request({
+      url: '/core/database/detailed',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 获取回收站数据
+   * @returns
+   */
+  getRecycle(params = {}) {
+    return request({
+      url: '/core/database/recycle',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 销毁数据
+   * @returns
+   */
+  delete(data) {
+    return request({
+      url: '/core/database/delete',
+      method: 'delete',
+      data
+    })
+  },
+
+  /**
+   * 恢复数据
+   * @returns
+   */
+  recovery(data) {
+    return request({
+      url: '/core/database/recovery',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 优化表
+   * @returns
+   */
+  optimize(data = {}) {
+    return request({
+      url: '/core/database/optimize',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 清理表碎片
+   * @returns
+   */
+  fragment(data = {}) {
+    return request({
+      url: '/core/database/fragment',
+      method: 'post',
+      data
+    })
+  }
+}

+ 121 - 0
src/api/system/dept.js

@@ -0,0 +1,121 @@
+import { request } from "@/utils/request.js";
+
+export default {
+  /**
+   * 获取部门树
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: "/core/dept/index",
+      method: "get",
+      params,
+    });
+  },
+
+  /**
+   * 获取部门领导列表
+   * @returns
+   */
+  getLeaderList(params = {}) {
+    return request({
+      url: "/core/dept/leaders",
+      method: "get",
+      params,
+    });
+  },
+
+  /**
+   * 新增部门领导
+   * @returns
+   */
+  addLeader(data = {}) {
+    return request({
+      url: "/core/dept/addLeader",
+      method: "post",
+      data,
+    });
+  },
+
+  /**
+   * 删除部门领导
+   * @returns
+   */
+  delLeader(data = {}) {
+    return request({
+      url: "/core/dept/delLeader",
+      method: "delete",
+      data,
+    });
+  },
+
+  /**
+   * 获取部门选择树
+   * @returns
+   */
+  tree() {
+    return request({
+      url: "/core/dept/index?tree=true",
+      method: "get",
+    });
+  },
+
+  /**
+   * 添加数据
+   * @returns
+   */
+  save(params = {}) {
+    return request({
+      url: "/core/dept/save",
+      method: "post",
+      data: params,
+    });
+  },
+
+  /**
+   * 删除数据
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: "/core/dept/destroy",
+      method: "delete",
+      data,
+    });
+  },
+
+  /**
+   * 修改数据
+   * @returns
+   */
+  update(id, params = {}) {
+    return request({
+      url: "/core/dept/update?id=" + id,
+      method: "put",
+      data: params,
+    });
+  },
+
+  /**
+   * 更改状态
+   * @returns
+   */
+  changeStatus(data = {}) {
+    return request({
+      url: "/core/dept/changeStatus",
+      method: "post",
+      data,
+    });
+  },
+  /**
+   * 根据部门ID获取游戏列表
+   * @returns
+   */
+  getGameListByDeptId(params = {}) {
+    return request({
+      url: "/core/dept/getGameListByDeptId",
+      method: "get",
+      params,
+    });
+  },
+};

+ 137 - 0
src/api/system/dict.js

@@ -0,0 +1,137 @@
+import { request } from '@/utils/request.js'
+
+export const dictType = {
+  /**
+   * 获取字典类型,无分页
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/core/dictType/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 添加字典类型
+   * @returns
+   */
+  save(params = {}) {
+    return request({
+      url: '/core/dictType/save',
+      method: 'post',
+      data: params
+    })
+  },
+
+  /**
+   * 删除字典类型
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/core/dictType/destroy',
+      method: 'delete',
+      data
+    })
+  },
+
+  /**
+   * 修改字典类型
+   * @returns
+   */
+  update(id, data = {}) {
+    return request({
+      url: '/core/dictType/update?id=' + id,
+      method: 'put',
+      data
+    })
+  },
+
+  /**
+   * 更改字典类型状态
+   * @returns
+   */
+  changeStatus(data = {}) {
+    return request({
+      url: '/core/dictType/changeStatus',
+      method: 'post',
+      data
+    })
+  }
+}
+
+export const dict = {
+  /**
+   * 快捷查询字典
+   * @param {*} params
+   * @returns
+   */
+  getDict(code) {
+    return request({
+      url: '/core/dataDict/index?code=' + code,
+      method: 'get'
+    })
+  },
+
+  /**
+   * 获取字典数据分页列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/core/dictData/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 添加字典数据
+   * @returns
+   */
+  addDictData(data = {}) {
+    return request({
+      url: '/core/dictData/save',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 删除字典数据
+   * @returns
+   */
+  destroyDictData(data) {
+    return request({
+      url: '/core/dictData/destroy',
+      method: 'delete',
+      data
+    })
+  },
+
+  /**
+   * 更新字典数据
+   * @returns
+   */
+  editDictData(id, data = {}) {
+    return request({
+      url: '/core/dictData/update?id=' + id,
+      method: 'put',
+      data
+    })
+  },
+
+  /**
+   * 更改字典状态
+   * @returns
+   */
+  changeStatus(data = {}) {
+    return request({
+      url: '/core/dictData/changeStatus',
+      method: 'post',
+      data
+    })
+  }
+}

+ 30 - 0
src/api/system/emailLog.js

@@ -0,0 +1,30 @@
+import { request } from '@/utils/request.js'
+
+/**
+ * 邮件日志接口
+ */
+export default {
+  /**
+   * 数据列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/core/email/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/core/email/destroy',
+      method: 'delete',
+      data
+    })
+  }
+}

+ 30 - 0
src/api/system/loginLog.js

@@ -0,0 +1,30 @@
+import { request } from '@/utils/request.js'
+
+/**
+ * 登录日志接口
+ */
+export default {
+  /**
+   * 数据列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/core/logs/getLoginLogPageList',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/core/logs/deleteLoginLog',
+      method: 'delete',
+      data
+    })
+  }
+}

+ 63 - 0
src/api/system/menu.js

@@ -0,0 +1,63 @@
+import { request } from '@/utils/request.js'
+
+export default {
+  /**
+   * 获取数据
+   * @returns
+   */
+  getList(params = {}) {
+    return request({
+      url: '/core/menu/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 可操作菜单
+   * @returns
+   */
+  accessMenu(params = {}) {
+    return request({
+      url: '/core/menu/accessMenu',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 添加数据
+   * @returns
+   */
+  save(params = {}) {
+    return request({
+      url: '/core/menu/save',
+      method: 'post',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/core/menu/destroy',
+      method: 'delete',
+      data
+    })
+  },
+
+  /**
+   * 更新数据
+   * @returns
+   */
+  update(id, data = {}) {
+    return request({
+      url: '/core/menu/update?id=' + id,
+      method: 'put',
+      data
+    })
+  }
+}

+ 17 - 0
src/api/system/monitor.js

@@ -0,0 +1,17 @@
+import { request } from '@/utils/request.js'
+
+/**
+ * 服务监控接口
+ */
+export default {
+  /**
+   * 获取服务器信息
+   * @returns
+   */
+  getServerInfo() {
+    return request({
+      url: '/core/system/monitor',
+      method: 'get'
+    })
+  }
+}

+ 77 - 0
src/api/system/notice.js

@@ -0,0 +1,77 @@
+import { request } from '@/utils/request.js'
+
+/**
+ * 通知公告接口
+ */
+export default {
+  /**
+   * 数据列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/core/notice/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 添加数据
+   * @returns
+   */
+  save(params = {}) {
+    return request({
+      url: '/core/notice/save',
+      method: 'post',
+      data: params
+    })
+  },
+
+  /**
+   * 读取数据
+   * @returns
+   */
+  read(id) {
+    return request({
+      url: '/core/notice/read?id=' + id,
+      method: 'get'
+    })
+  },
+
+  /**
+   * 删除数据
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/core/notice/destroy',
+      method: 'delete',
+      data
+    })
+  },
+
+  /**
+   * 修改数据
+   * @returns
+   */
+  update(id, data = {}) {
+    return request({
+      url: '/core/notice/update?id=' + id,
+      method: 'put',
+      data
+    })
+  },
+
+  /**
+   * 修改状态
+   * @returns
+   */
+  changeStatus(data = {}) {
+    return request({
+      url: '/core/notice/changeStatus',
+      method: 'post',
+      data
+    })
+  }
+}

+ 30 - 0
src/api/system/operLog.js

@@ -0,0 +1,30 @@
+import { request } from '@/utils/request.js'
+
+/**
+ * 操作日志接口
+ */
+export default {
+  /**
+   * 数据列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/core/logs/getOperLogPageList',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/core/logs/deleteOperLog',
+      method: 'delete',
+      data
+    })
+  }
+}

+ 77 - 0
src/api/system/post.js

@@ -0,0 +1,77 @@
+import { request } from '@/utils/request.js'
+
+/**
+ * 岗位数据接口
+ */
+export default {
+  /**
+   * 数据列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/core/post/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 读取数据
+   * @returns
+   */
+  read(id) {
+    return request({
+      url: '/core/post/read?id=' + id,
+      method: 'get'
+    })
+  },
+
+  /**
+   * 添加数据
+   * @returns
+   */
+  save(params = {}) {
+    return request({
+      url: '/core/post/save',
+      method: 'post',
+      data: params
+    })
+  },
+
+  /**
+   * 修改数据
+   * @returns
+   */
+  update(id, data = {}) {
+    return request({
+      url: '/core/post/update?id=' + id,
+      method: 'put',
+      data
+    })
+  },
+
+  /**
+   * 更改状态
+   * @returns
+   */
+  changeStatus(data = {}) {
+    return request({
+      url: '/core/post/changeStatus',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 删除数据
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/core/post/destroy',
+      method: 'delete',
+      data
+    })
+  }
+}

+ 109 - 0
src/api/system/role.js

@@ -0,0 +1,109 @@
+import { request } from '@/utils/request.js'
+
+export default {
+  /**
+   * 获取数据列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/core/role/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 通过角色获取菜单
+   * @returns
+   */
+  getMenuByRole(id) {
+    return request({
+      url: '/core/role/getMenuByRole?id=' + id,
+      method: 'get'
+    })
+  },
+
+  /**
+   * 通过角色获取部门
+   * @returns
+   */
+  getDeptByRole(id) {
+    return request({
+      url: '/core/role/getDeptByRole?id=' + id,
+      method: 'get'
+    })
+  },
+
+  /**
+   * 添加数据
+   * @returns
+   */
+  save(data = {}) {
+    return request({
+      url: '/core/role/save',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 删除数据
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/core/role/destroy',
+      method: 'delete',
+      data
+    })
+  },
+
+  /**
+   * 更新数据
+   * @returns
+   */
+  update(id, data = {}) {
+    return request({
+      url: '/core/role/update?id=' + id,
+      method: 'put',
+      data
+    })
+  },
+
+  /**
+   * 更新菜单权限
+   * @returns
+   */
+  updateMenuPermission(id, data) {
+    return request({
+      url: '/core/role/menuPermission?id=' + id,
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 更新数据权限
+   * @returns
+   */
+  updateDataPermission(id, data) {
+    return request({
+      url: '/core/role/dataPermission?id=' + id,
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 更改数据状态
+   * @returns
+   */
+  changeStatus(params = {}) {
+    return request({
+      url: '/core/role/changeStatus',
+      method: 'post',
+      data: params
+    })
+  }
+}

+ 146 - 0
src/api/system/user.js

@@ -0,0 +1,146 @@
+import { request } from "@/utils/request.js";
+
+export default {
+  /**
+   * 获取数据列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: "/core/user/index",
+      method: "get",
+      params,
+    });
+  },
+
+  /**
+   * 读取数据
+   * @returns
+   */
+  read(id) {
+    return request({
+      url: "/core/user/read?id=" + id,
+      method: "get",
+    });
+  },
+
+  /**
+   * 添加数据
+   * @returns
+   */
+  save(params = {}) {
+    return request({
+      url: "/core/user/save",
+      method: "post",
+      data: params,
+    });
+  },
+
+  /**
+   * 删除数据
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: "/core/user/destroy",
+      method: "delete",
+      data,
+    });
+  },
+
+  /**
+   * 更新数据
+   * @returns
+   */
+  update(id, data = {}) {
+    return request({
+      url: "/core/user/update?id=" + id,
+      method: "put",
+      data,
+    });
+  },
+
+  /**
+   * 更改数据状态
+   * @returns
+   */
+  changeStatus(data = {}) {
+    return request({
+      url: "/core/user/changeStatus",
+      method: "post",
+      data,
+    });
+  },
+
+  /**
+   * 清除用户缓存
+   * @returns
+   */
+  clearCache(params = {}) {
+    return request({
+      url: "/core/user/clearCache",
+      method: "post",
+      data: params,
+    });
+  },
+
+  /**
+   * 设置用户首页
+   * @returns
+   */
+  setHomePage(data = {}) {
+    return request({
+      url: "/core/user/setHomePage",
+      method: "post",
+      data,
+    });
+  },
+
+  /**
+   * 设置用户权限
+   * @returns
+   */
+  setUserPermission(data = {}) {
+    return request({
+      url: "/core/user/setUserPermission",
+      method: "post",
+      data,
+    });
+  },
+
+  /**
+   * 初始化用户密码
+   * @returns
+   */
+  initUserPassword(data) {
+    return request({
+      url: "/core/user/initUserPassword",
+      method: "post",
+      data,
+    });
+  },
+
+  /**
+   * 用户更新个人资料
+   * @returns
+   */
+  updateInfo(data = {}) {
+    return request({
+      url: "/core/user/updateInfo",
+      method: "post",
+      data,
+    });
+  },
+
+  /**
+   * 用户修改密码
+   * @returns
+   */
+  modifyPassword(data = {}) {
+    return request({
+      url: "/core/user/modifyPassword",
+      method: "post",
+      data,
+    });
+  },
+};

+ 113 - 0
src/api/tool/crontab.js

@@ -0,0 +1,113 @@
+import { request } from '@/utils/request.js'
+
+/**
+ * 定时任务接口
+ */
+export default {
+  /**
+   * 数据列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/tool/crontab/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 日志列表
+   * @returns
+   */
+  getLogPageList(params = {}) {
+    return request({
+      url: '/tool/crontab/logPageList',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 删除定时任务日志
+   * @returns
+   */
+  deleteLog(data) {
+    return request({
+      url: '/tool/crontab/deleteCrontabLog',
+      method: 'delete',
+      data
+    })
+  },
+
+  /**
+   * 立刻执行一次定时任务
+   * @returns
+   */
+  run(data = {}) {
+    return request({
+      url: '/tool/crontab/run',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 读取数据
+   * @returns
+   */
+  read(id) {
+    return request({
+      url: '/tool/crontab/read?id=' + id,
+      method: 'get'
+    })
+  },
+
+  /**
+   * 添加
+   * @returns
+   */
+  save(data = {}) {
+    return request({
+      url: '/tool/crontab/save',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 删除
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/tool/crontab/destroy',
+      method: 'delete',
+      data
+    })
+  },
+
+  /**
+   * 更新数据
+   * @returns
+   */
+  update(id, params = {}) {
+    return request({
+      url: '/tool/crontab/update?id=' + id,
+      method: 'put',
+      data: params
+    })
+  },
+
+  /**
+   * 更改状态
+   * @returns
+   */
+  changeStatus(data = {}) {
+    return request({
+      url: '/tool/crontab/changeStatus',
+      method: 'post',
+      data
+    })
+  }
+}

+ 135 - 0
src/api/tool/generate.js

@@ -0,0 +1,135 @@
+import { request } from '@/utils/request.js'
+
+export default {
+  /**
+   * 数据列表
+   * @returns
+   */
+  getPageList(params = {}) {
+    return request({
+      url: '/tool/code/index',
+      method: 'get',
+      params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @returns
+   */
+  destroy(data) {
+    return request({
+      url: '/tool/code/destroy',
+      method: 'delete',
+      data
+    })
+  },
+
+  /**
+   * 编辑信息
+   * @returns
+   */
+  update(id, data = {}) {
+    return request({
+      url: '/tool/code/update?id=' + id,
+      method: 'put',
+      data
+    })
+  },
+
+  /**
+   * 读取信息
+   */
+  readTable(id) {
+    return request({
+      url: '/tool/code/read?id=' + id,
+      method: 'get'
+    })
+  },
+
+  /**
+   * 生成代码
+   * @returns
+   */
+  generateCode(data = {}) {
+    return request({
+      url: '/tool/code/generate',
+      method: 'post',
+      responseType: 'blob',
+      timeout: 20 * 1000,
+      data
+    })
+  },
+
+  /**
+   * 生成到文件
+   * @returns
+   */
+  generateFile(data = {}) {
+    return request({
+      url: '/tool/code/generateFile',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 装载数据表
+   * @returns
+   */
+  loadTable(data = {}) {
+    return request({
+      url: '/tool/code/loadTable',
+      method: 'post',
+      data
+    })
+  },
+
+  /**
+   * 同步数据表
+   * @returns
+   */
+  sync(id) {
+    return request({
+      url: '/tool/code/sync?id=' + id,
+      method: 'post'
+    })
+  },
+
+  /**
+   * 预览代码
+   * @returns
+   */
+  preview(id) {
+    return request({
+      url: '/tool/code/preview?id=' + id,
+      method: 'get'
+    })
+  },
+
+  // 获取表中字段信息
+  getTableColumns(params = {}) {
+    return request({
+      url: '/tool/code/getTableColumns',
+      method: 'get',
+      params
+    })
+  },
+
+  // 获取数据源列表
+  getDataSourceList(params = {}) {
+    return request({
+      url: '/tool/code/getDataSourceList',
+      method: 'get',
+      params
+    })
+  },
+
+  // 获取所有模型
+  getModels() {
+    return request({
+      url: '/tool/code/getModels',
+      method: 'get'
+    })
+  }
+}

+ 176 - 0
src/assets/404.svg

@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="770px" height="456px" viewBox="0 0 770 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
+    <title>6</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <path d="M451.167458,89.4511247 C403.062267,29.8170416 338.891681,0 258.655699,0 C138.301726,0 69.263862,60.1782766 27.9579265,152.101254 C-13.3480089,244.024231 -12.6661889,369.858107 55.6494632,409.696073 C123.965115,449.534039 210.08756,459.743134 340.957927,438.489218 C471.828293,417.235303 508.472089,464.890133 589.496232,451.689675 C670.520376,438.489218 748.359885,414.0324 766.111966,329.133852 C783.864046,244.235303 714.426288,177.226358 677.67078,152.101254 C640.915272,126.97615 569.728461,175.208649 519.030321,160.235303 C485.231561,150.253072 462.610607,126.658346 451.167458,89.4511247 Z" id="path-1"></path>
+        <path d="M0.816264722,0 L370.714266,0 L370.714266,180.257104 L402.92544,180.257104 C424.638356,218.017298 440.878062,240.166012 451.644559,246.703245 L119.609274,243.521057 C112.14379,243.449507 105.100966,240.045172 100.407325,234.239285 C89.3772632,220.595444 81.4909058,210.013897 76.7482527,202.494643 C68.1135311,188.804698 66.7639588,180.257104 51.9095874,180.257104 C37.055216,180.257104 30.8879728,215.663472 26.2206784,229.536211 C21.5533841,243.408951 4.54747351e-13,240.685607 4.54747351e-13,229.536211 C4.54747351e-13,222.103281 0.272088241,145.59121 0.816264722,0 Z" id="path-3"></path>
+        <polygon id="path-5" points="0 25.9764499 26.0411111 2.29150032e-13 52.9088048 25.9764499"></polygon>
+        <polygon id="path-7" points="-2.27373675e-13 28.2395915 28.1433883 3.41060513e-13 54.0330976 28.2395915"></polygon>
+        <path d="M3.53184776,0 L61.4681522,0 C63.1250065,4.9985658e-15 64.4681522,1.34314575 64.4681522,3 C64.4681522,3.16257855 64.4549364,3.32488807 64.4286352,3.48532508 L55.4122418,58.4853251 C55.1745077,59.9355031 53.921294,61 52.4517588,61 L12.5482412,61 C11.078706,61 9.82549232,59.9355031 9.58775821,58.4853251 L0.571364767,3.48532508 C0.303327126,1.85029547 1.41149307,0.307554646 3.04652268,0.0395170047 C3.20695969,0.0132158559 3.36926922,-1.30240244e-15 3.53184776,0 Z" id="path-9"></path>
+        <path d="M-1.42108547e-14,115.48446 C1.32743544,94.0102656 2.89289856,78.9508436 4.69638937,70.3061937 C8.43003277,52.4097675 15.5176097,37.8448008 19.4787027,30.195863 C29.7253967,10.409323 39.7215535,5.31301339 44.6820442,2.63347577 C49.6425348,-0.0460618448 60.3007481,-1.62222357 66.327433,2.63347577 C72.3541179,6.88917511 74.5668372,13.0533931 73.7454921,23.1564165 C72.924147,33.2594398 65.469448,39.1497458 58.0193289,42.7343523 C50.5692098,46.3189588 31.0128594,60.1734323 19.4787027,74.1118722 C11.7892649,83.4041655 5.29636401,97.195028 -1.42108547e-14,115.48446 Z" id="path-11"></path>
+        <path d="M0,61.382873 C12.627563,35.4721831 22.8842273,18.9178104 30.7699929,11.7197549 C42.5986412,0.922671591 57.9238693,-1.5327187 66.3547392,0.814866828 C74.7856091,3.16245236 78.9526569,14.6315037 74.3469666,21.3628973 C69.7412762,28.0942909 65.4378728,28.0568843 50.8423324,30.6914365 C36.246792,33.3259886 29.5659376,36.8930178 23.8425136,39.4010039 C21.5824174,40.3913708 15.331987,43.4769377 10.1725242,48.4356558 C7.80517763,50.7108935 4.41433624,55.0266325 0,61.382873 Z" id="path-13"></path>
+        <path d="M-2.08995462,65.6474954 C12.5975781,38.2270573 23.8842273,20.9178104 31.7699929,13.7197549 C43.5986412,2.92267159 58.9238693,0.467281299 67.3547392,2.81486683 C75.7856091,5.16245236 79.9526569,16.6315037 75.3469666,23.3628973 C70.7412762,30.0942909 66.4378728,30.0568843 51.8423324,32.6914365 C37.246792,35.3259886 30.5659376,38.8930178 24.8425136,41.4010039 C22.5824174,42.3913708 13.2420323,47.7415601 8.08256956,52.7002782 C5.71522301,54.9755159 2.32438162,59.2912549 -2.08995462,65.6474954 Z" id="path-15"></path>
+        <path d="M70.3618111,117.305105 C65.1514723,93.5149533 59.5592828,76.7727476 53.5852425,67.0784883 C44.6241821,52.5370993 33.2521675,43.1631445 21.9273327,38.7089848 C10.6024978,34.2548251 1.37005489,28.3143707 0.166250333,19.5991494 C-1.03755422,10.8839281 4.30184276,1.89650161 15.9982131,0.359853321 C27.6945835,-1.17679496 39.680528,1.89650161 50.3232751,15.6556441 C60.9660221,29.4147866 71.7898492,71.0503233 71.7898492,87.5111312 C71.7898492,98.4850031 71.3138365,108.416328 70.3618111,117.305105 Z" id="path-17"></path>
+        <path d="M40.4361627,109.727577 C42.2080966,71.0333394 41.2052946,44.753324 37.4277569,30.8875312 C31.7614504,10.088842 22.8541813,-1.27827958 11.3728741,0.114578571 C-0.108432993,1.50743672 -2.5866861,11.539269 2.54272088,19.2423116 C7.67212787,26.9453541 22.1964111,48.5363293 27.3543068,61.4631547 C30.7929039,70.0810384 35.1535225,86.1691793 40.4361627,109.727577 Z" id="path-19"></path>
+        <path d="M86.8630745,43.7959111 C72.5806324,23.5140129 56.8667378,10.125403 39.7213908,3.6300812 C14.0033702,-6.11290144 -7.10542736e-15,5.90110838 -7.10542736e-15,14.52167 C-7.10542736e-15,23.1422316 6.80949202,28.0268155 17.0489556,28.0268155 C27.2884192,28.0268155 43.7234658,26.0070237 58.8280258,34.5737997 C68.8977326,40.2849837 79.1842128,49.927944 89.6874666,63.5026805 L86.8630745,43.7959111 Z" id="path-21"></path>
+        <circle id="path-23" cx="42" cy="42" r="42"></circle>
+    </defs>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-2046.000000, -1809.000000)">
+            <g id="6" transform="translate(2046.223123, 1809.764697)">
+                <g id="编组-89" transform="translate(0.109175, 0.235303)">
+                    <mask id="mask-2" fill="white">
+                        <use xlink:href="#path-1"></use>
+                    </mask>
+                    <use id="路径-307" fill="#F3F7FF" xlink:href="#path-1"></use>
+                    <rect id="矩形" fill="#D0DEFE" mask="url(#mask-2)" x="0" y="362" width="791" height="112"></rect>
+                    <rect id="矩形" fill="#C4D6FF" mask="url(#mask-2)" transform="translate(395.500000, 353.500000) scale(1, -1) translate(-395.500000, -353.500000) " x="0" y="345" width="791" height="17"></rect>
+                </g>
+                <rect id="矩形" stroke="#979797" fill="#D8D8D8" x="632.609175" y="381.735303" width="39" height="10"></rect>
+                <rect id="矩形" stroke="#979797" fill="#D8D8D8" x="632.609175" y="402.735303" width="39" height="10"></rect>
+                <rect id="矩形" stroke="#979797" fill="#D8D8D8" x="628.609175" y="392.735303" width="39" height="10"></rect>
+                <g id="编组-88" transform="translate(547.109175, 141.235303)">
+                    <rect id="矩形" fill="#BCD4FF" x="0" y="0" width="144" height="281"></rect>
+                    <rect id="矩形" fill="#EDF4FF" x="5" y="10.5" width="131" height="262"></rect>
+                    <rect id="矩形" fill="#9EBEF8" x="106" y="10.5" width="30" height="262"></rect>
+                    <rect id="矩形" fill="#BCD4FF" x="56" y="136" width="80" height="8"></rect>
+                    <rect id="矩形" fill="#BCD4FF" x="56" y="203" width="80" height="8"></rect>
+                    <g id="编组-87" transform="translate(63.000000, 153.000000)">
+                        <rect id="矩形" fill="#FFECC8" x="29" y="0" width="40" height="50"></rect>
+                        <rect id="矩形" fill="#FFE2AC" x="58" y="0" width="11" height="50"></rect>
+                        <rect id="矩形" fill="#D3E1FF" x="14" y="0" width="40" height="50"></rect>
+                        <rect id="矩形" fill="#BDD2FF" x="43" y="0" width="11" height="50"></rect>
+                        <rect id="矩形" fill="#FFECC8" x="0" y="8" width="40" height="42"></rect>
+                        <rect id="矩形" fill="#FFE2AC" x="29" y="8" width="11" height="42"></rect>
+                    </g>
+                    <g id="编组-87" transform="translate(63.000000, 222.000000)">
+                        <rect id="矩形" fill="#FFECC8" x="29" y="0" width="40" height="50"></rect>
+                        <rect id="矩形" fill="#FFE2AC" x="58" y="0" width="11" height="50"></rect>
+                        <rect id="矩形" fill="#D3E1FF" x="14" y="0" width="40" height="50"></rect>
+                        <rect id="矩形" fill="#BDD2FF" x="43" y="0" width="11" height="50"></rect>
+                        <rect id="矩形" fill="#FFECC8" x="0" y="8" width="40" height="42"></rect>
+                        <rect id="矩形" fill="#FFE2AC" x="29" y="8" width="11" height="42"></rect>
+                    </g>
+                    <g id="编组-87" transform="translate(63.000000, 86.000000)">
+                        <rect id="矩形" fill="#FFECC8" x="29" y="0" width="40" height="50"></rect>
+                        <rect id="矩形" fill="#FFE2AC" x="58" y="0" width="11" height="50"></rect>
+                        <rect id="矩形" fill="#D3E1FF" x="14" y="0" width="40" height="50"></rect>
+                        <rect id="矩形" fill="#BDD2FF" x="43" y="0" width="11" height="50"></rect>
+                        <rect id="矩形" fill="#FFECC8" x="0" y="8" width="40" height="42"></rect>
+                        <rect id="矩形" fill="#FFE2AC" x="29" y="8" width="11" height="42"></rect>
+                    </g>
+                </g>
+                <path d="M206.109175,130.235303 L586.109175,130.235303 C596.0503,130.235303 604.109175,138.294177 604.109175,148.235303 L604.109175,417.235303 L604.109175,417.235303 L188.109175,417.235303 L188.109175,148.235303 C188.109175,138.294177 196.168049,130.235303 206.109175,130.235303 Z" id="矩形" fill="#DDE9FF"></path>
+                <path d="M206.109175,130.235303 L586.109175,130.235303 C596.0503,130.235303 604.109175,138.294177 604.109175,148.235303 L604.109175,163.235303 L604.109175,163.235303 L188.109175,163.235303 L188.109175,148.235303 C188.109175,138.294177 196.168049,130.235303 206.109175,130.235303 Z" id="矩形" fill="#FFECC8"></path>
+                <path d="M206.109175,130.235303 L586.109175,130.235303 C596.0503,130.235303 604.109175,138.294177 604.109175,148.235303 L604.109175,160.235303 L604.109175,160.235303 L188.109175,160.235303 L188.109175,148.235303 C188.109175,138.294177 196.168049,130.235303 206.109175,130.235303 Z" id="矩形" fill="#A4C3FC"></path>
+                <circle id="椭圆形" fill="#FFBB3C" cx="210" cy="146" r="4"></circle>
+                <circle id="椭圆形" fill="#ECF2FF" cx="223.109175" cy="145.235303" r="4"></circle>
+                <g id="编组-86" transform="translate(210.109175, 178.235303)">
+                    <mask id="mask-4" fill="white">
+                        <use xlink:href="#path-3"></use>
+                    </mask>
+                    <use id="路径-289" fill="#FFFFFF" xlink:href="#path-3"></use>
+                    <rect id="矩形" fill="#ECF2FF" mask="url(#mask-4)" x="50.8162647" y="180.401344" width="412" height="87"></rect>
+                    <polygon id="路径" fill="#FFEAC2" fill-rule="nonzero" mask="url(#mask-4)" points="361.449861 8.85304449 361.449861 180.761462 360.449861 180.761462 360.449 9.853 14.449 9.853 14.449 223.853 27.9199219 223.853044 27.9199219 224.853044 13.4498606 224.853044 13.4498606 8.85304449"></polygon>
+                </g>
+                <path d="M333.259175,333.235303 L333.259175,308.935303 L350.659175,308.935303 L350.659175,298.885303 L333.259175,298.885303 L333.259175,226.135303 L321.709175,226.135303 L267.709175,297.235303 L267.709175,308.935303 L321.559175,308.935303 L321.559175,333.235303 L333.259175,333.235303 Z M321.559175,298.885303 L278.059175,298.885303 L321.109175,242.185303 L321.559175,242.185303 L321.559175,298.885303 Z M399.109175,335.335303 C411.859175,335.335303 421.459175,329.635303 428.059175,318.385303 C433.759175,308.785303 436.609175,295.885303 436.609175,279.685303 C436.609175,263.485303 433.759175,250.585303 428.059175,240.985303 C421.459175,229.585303 411.859175,224.035303 399.109175,224.035303 C386.209175,224.035303 376.609175,229.585303 370.159175,240.985303 C364.459175,250.585303 361.609175,263.485303 361.609175,279.685303 C361.609175,295.885303 364.459175,308.785303 370.159175,318.385303 C376.609175,329.635303 386.209175,335.335303 399.109175,335.335303 Z M399.109175,324.835303 C389.509175,324.835303 382.609175,319.585303 378.409175,309.385303 C375.409175,302.035303 373.909175,292.135303 373.909175,279.685303 C373.909175,267.085303 375.409175,257.185303 378.409175,249.985303 C382.609175,239.635303 389.509175,234.535303 399.109175,234.535303 C408.709175,234.535303 415.609175,239.635303 419.809175,249.985303 C422.809175,257.185303 424.459175,267.085303 424.459175,279.685303 C424.459175,292.135303 422.809175,302.035303 419.809175,309.385303 C415.609175,319.585303 408.709175,324.835303 399.109175,324.835303 Z M513.259175,333.235303 L513.259175,308.935303 L530.659175,308.935303 L530.659175,298.885303 L513.259175,298.885303 L513.259175,226.135303 L501.709175,226.135303 L447.709175,297.235303 L447.709175,308.935303 L501.559175,308.935303 L501.559175,333.235303 L513.259175,333.235303 Z M501.559175,298.885303 L458.059175,298.885303 L501.109175,242.185303 L501.559175,242.185303 L501.559175,298.885303 Z" id="404" fill="#FFEAC2" fill-rule="nonzero"></path>
+                <path d="M330.259175,330.235303 L330.259175,305.935303 L347.659175,305.935303 L347.659175,295.885303 L330.259175,295.885303 L330.259175,223.135303 L318.709175,223.135303 L264.709175,294.235303 L264.709175,305.935303 L318.559175,305.935303 L318.559175,330.235303 L330.259175,330.235303 Z M318.559175,295.885303 L275.059175,295.885303 L318.109175,239.185303 L318.559175,239.185303 L318.559175,295.885303 Z M396.109175,332.335303 C408.859175,332.335303 418.459175,326.635303 425.059175,315.385303 C430.759175,305.785303 433.609175,292.885303 433.609175,276.685303 C433.609175,260.485303 430.759175,247.585303 425.059175,237.985303 C418.459175,226.585303 408.859175,221.035303 396.109175,221.035303 C383.209175,221.035303 373.609175,226.585303 367.159175,237.985303 C361.459175,247.585303 358.609175,260.485303 358.609175,276.685303 C358.609175,292.885303 361.459175,305.785303 367.159175,315.385303 C373.609175,326.635303 383.209175,332.335303 396.109175,332.335303 Z M396.109175,321.835303 C386.509175,321.835303 379.609175,316.585303 375.409175,306.385303 C372.409175,299.035303 370.909175,289.135303 370.909175,276.685303 C370.909175,264.085303 372.409175,254.185303 375.409175,246.985303 C379.609175,236.635303 386.509175,231.535303 396.109175,231.535303 C405.709175,231.535303 412.609175,236.635303 416.809175,246.985303 C419.809175,254.185303 421.459175,264.085303 421.459175,276.685303 C421.459175,289.135303 419.809175,299.035303 416.809175,306.385303 C412.609175,316.585303 405.709175,321.835303 396.109175,321.835303 Z M510.259175,330.235303 L510.259175,305.935303 L527.659175,305.935303 L527.659175,295.885303 L510.259175,295.885303 L510.259175,223.135303 L498.709175,223.135303 L444.709175,294.235303 L444.709175,305.935303 L498.559175,305.935303 L498.559175,330.235303 L510.259175,330.235303 Z M498.559175,295.885303 L455.059175,295.885303 L498.109175,239.185303 L498.559175,239.185303 L498.559175,295.885303 Z" id="404" fill="#ACC9FF" fill-rule="nonzero"></path>
+                <polygon id="路径-298" fill="#6EA1FF" fill-rule="nonzero" points="369.741481 26.3549544 369.741481 145.784171 368.741481 145.784171 368.741481 26.3549544"></polygon>
+                <g id="编组-113" transform="translate(343.200370, 145.784171)">
+                    <mask id="mask-6" fill="white">
+                        <use xlink:href="#path-5"></use>
+                    </mask>
+                    <use id="路径-299" fill="#FFD078" xlink:href="#path-5"></use>
+                    <polygon id="路径-299" fill="#FFBB3C" mask="url(#mask-6)" points="-3 25.9764499 23.0411111 1.77635684e-15 49.9088048 25.9764499"></polygon>
+                </g>
+                <polygon id="路径-300" fill="#6EA1FF" fill-rule="nonzero" points="254.30695 -0.00143864693 255.306945 0.00143864694 255.109173 68.7367415 254.109177 68.7338642"></polygon>
+                <g id="编组-112" transform="translate(226.663559, 65.717848)">
+                    <mask id="mask-8" fill="white">
+                        <use xlink:href="#path-7"></use>
+                    </mask>
+                    <use id="路径-301" fill="#D2E2FF" xlink:href="#path-7"></use>
+                    <polygon id="路径-301" fill="#A4C3FC" mask="url(#mask-8)" points="-3 28.2395915 25.1433883 -1.13686838e-13 51.0330976 28.2395915"></polygon>
+                </g>
+                <path d="M464.109175,72.2353029 L574.109175,72.2353029 C578.527453,72.2353029 582.109175,75.8170249 582.109175,80.2353029 L582.109175,152.269143 L582.109175,152.269143 L602.747625,174.760621 L464.163722,175.18059 C454.222643,175.210716 446.139383,167.1763 446.109258,157.23522 C446.109203,157.217038 446.109175,157.198855 446.109175,157.180672 L446.109175,90.2353029 C446.109175,80.2941774 454.168049,72.2353029 464.109175,72.2353029 Z" id="矩形" fill="#FFECC8"></path>
+                <path d="M460.109175,69.2353029 L570.109175,69.2353029 C574.527453,69.2353029 578.109175,72.8170249 578.109175,77.2353029 L578.109175,149.269143 L578.109175,149.269143 L598.747625,171.760621 L460.163722,172.18059 C450.222643,172.210716 442.139383,164.1763 442.109258,154.23522 C442.109203,154.217038 442.109175,154.198855 442.109175,154.180672 L442.109175,87.2353029 C442.109175,77.2941774 450.168049,69.2353029 460.109175,69.2353029 Z" id="矩形" fill="#EBF2FF"></path>
+                <rect id="矩形" fill="#FFFFFF" x="480" y="95" width="7" height="64"></rect>
+                <rect id="矩形" fill="#FFFFFF" x="497.109175" y="95" width="7" height="64"></rect>
+                <rect id="矩形" fill="#FFFFFF" x="514.109175" y="95" width="7" height="64"></rect>
+                <rect id="矩形" fill="#FFFFFF" x="530.109175" y="95" width="7" height="64"></rect>
+                <rect id="矩形" fill="#FFFFFF" x="546.109175" y="95" width="7" height="64"></rect>
+                <polygon id="路径-302" fill="#A4C3FC" fill-rule="nonzero" points="466.970801 86.0695627 466.97 158.272 566.971883 158.272396 566.971883 159.272396 465.970801 159.272396 465.970801 86.0695627"></polygon>
+                <polygon id="路径-304" fill="#979797" fill-rule="nonzero" points="559.240013 152.472555 559.909745 151.729952 567.687435 158.744424 559.937708 166.917681 559.21205 166.229626 566.256 158.8"></polygon>
+                <path d="M547.776877,151.235303 L657.776877,151.235303 C662.195155,151.235303 665.776877,154.817025 665.776877,159.235303 L665.776877,231.269143 L665.776877,231.269143 L686.415326,253.760621 L547.831424,254.18059 C537.890344,254.210716 529.807085,246.1763 529.776959,236.23522 C529.776904,236.217038 529.776877,236.198855 529.776877,236.180672 L529.776877,169.235303 C529.776877,159.294177 537.835751,151.235303 547.776877,151.235303 Z" id="矩形" fill="#A4C3FC" transform="translate(608.096101, 202.735303) scale(-1, 1) translate(-608.096101, -202.735303) "></path>
+                <path d="M542.776877,150.235303 L652.776877,150.235303 C657.195155,150.235303 660.776877,153.817025 660.776877,158.235303 L660.776877,230.269143 L660.776877,230.269143 L681.415326,252.760621 L542.831424,253.18059 C532.890344,253.210716 524.807085,245.1763 524.776959,235.23522 C524.776904,235.217038 524.776877,235.198855 524.776877,235.180672 L524.776877,168.235303 C524.776877,158.294177 532.835751,150.235303 542.776877,150.235303 Z" id="矩形" fill="#FFCA67" transform="translate(603.096101, 201.735303) scale(-1, 1) translate(-603.096101, -201.735303) "></path>
+                <path d="M551.888365,105.031459 C555.290806,103.139777 558.513795,102.897668 562.237517,104.467631 L562.588104,104.620129 L562.17909,105.532657 C558.599379,103.928154 555.612548,104.105059 552.37429,105.905459 C550.368282,107.020755 548.58771,108.45472 545.16394,111.609463 L541.614214,114.898486 C538.015181,118.209826 536.087942,119.845252 533.11225,122.086913 C532.782184,122.335559 532.450805,122.581445 532.117803,122.824718 C528.104792,125.756407 523.934988,126.987135 519.313532,126.876779 C516.035171,126.798495 513.270144,126.221396 508.17289,124.737029 L505.737532,124.022849 C497.810115,121.733418 494.471662,121.366012 490.348408,122.889971 C482.286296,125.869735 475.026188,137.650266 468.664891,158.243664 L468.457777,158.918311 L467.501306,158.626481 C473.997113,137.336567 481.463921,125.107569 490.001728,121.951987 C494.55996,120.267261 498.129316,120.741123 506.945337,123.333314 L508.921,123.912647 C513.638819,125.272395 516.27064,125.803833 519.337404,125.877064 C523.744193,125.982294 527.697642,124.815424 531.527904,122.017241 L532.020296,121.654721 L532.510552,121.288189 C535.634194,118.935074 537.593599,117.254212 541.615416,113.537015 L544.497926,110.863327 C547.974881,107.659837 549.794043,106.195856 551.888365,105.031459 Z" id="路径-303" fill="#FFBB3C" fill-rule="nonzero"></path>
+                <polygon id="路径-305" fill="#A4C3FC" fill-rule="nonzero" points="458.750713 92.4640098 466.468831 85.3932668 474.275626 92.4620486 473.604426 93.2033249 466.472 86.745 459.42622 93.2013637"></polygon>
+                <g id="编组-81" transform="translate(50.109175, 134.235303)">
+                    <g id="编组-63" transform="translate(63.914093, 222.107327)">
+                        <mask id="mask-10" fill="white">
+                            <use xlink:href="#path-9"></use>
+                        </mask>
+                        <use id="矩形" fill="#DDE9FF" xlink:href="#path-9"></use>
+                        <path d="M1.20882698,0 L63.4052217,0 C64.4023405,3.36954592e-15 65.3342971,0.495421402 65.8920292,1.32196893 L67.6990928,4 C68.491521,5.17436234 68.1819023,6.76876112 67.0075399,7.56118935 C66.5836904,7.84719165 66.0840393,8 65.5727217,8 L2.06042429,8 C0.819095645,8 -0.294028853,7.23549708 -0.7396257,6.076903 L-1.5384054,4 C-2.12194354,2.48274545 -1.36501684,0.779716485 0.152237704,0.196178338 C0.489411271,0.0665009295 0.84757604,9.54539142e-16 1.20882698,0 Z" id="矩形" fill="#FFBB3C" mask="url(#mask-10)"></path>
+                    </g>
+                    <g id="编组-103" transform="translate(90.000000, 84.000000)">
+                        <mask id="mask-12" fill="white">
+                            <use xlink:href="#path-11"></use>
+                        </mask>
+                        <use id="路径-246" fill="#EAFFF3" xlink:href="#path-11"></use>
+                        <path d="M-1.42108547e-14,119.48446 C1.32743544,98.0102656 2.89289856,82.9508436 4.69638937,74.3061937 C8.43003277,56.4097675 15.5176097,41.8448008 19.4787027,34.195863 C29.7253967,14.409323 39.7215535,9.31301339 44.6820442,6.63347577 C49.6425348,3.95393816 60.3007481,2.37777643 66.327433,6.63347577 C72.3541179,10.8891751 74.5668372,17.0533931 73.7454921,27.1564165 C72.924147,37.2594398 65.469448,43.1497458 58.0193289,46.7343523 C50.5692098,50.3189588 31.0128594,64.1734323 19.4787027,78.1118722 C11.7892649,87.4041655 5.29636401,101.195028 -1.42108547e-14,119.48446 Z" id="路径-246" fill="#D3F0E0" mask="url(#mask-12)"></path>
+                        <path d="M61.0623172,22.0501917 C61.3287364,21.9775593 61.6035919,22.1346545 61.6762243,22.4010736 C61.7488567,22.6674927 61.5917615,22.9423483 61.3253424,23.0149807 C30.6460939,31.3788982 10.4195539,62.2160822 0.685726462,115.62224 C0.636212326,115.893907 0.375843567,116.073997 0.104176562,116.024483 C-0.167490443,115.974969 -0.347580926,115.7146 -0.29806679,115.442933 C9.49743654,61.6983811 29.9375632,30.535565 61.0623172,22.0501917 Z" id="路径-251" fill="#9FC8B1" fill-rule="nonzero" mask="url(#mask-12)"></path>
+                        <path d="M53.2988281,45.3242187 C23.2203776,62.4189453 5.8733724,84.8946126 1.2578125,112.751221 C31.0439453,72.423808 48.9638672,50.9959922 55.0175781,48.4677734 C61.0712891,45.9395547 60.4983724,44.8917031 53.2988281,45.3242187 Z" id="路径-358" fill="#C4E0D1" mask="url(#mask-12)"></path>
+                    </g>
+                    <g id="编组-102" transform="translate(112.698267, 46.543175)">
+                        <mask id="mask-14" fill="white">
+                            <use xlink:href="#path-13"></use>
+                        </mask>
+                        <use id="路径-247" fill="#EAFFF3" xlink:href="#path-13"></use>
+                        <mask id="mask-16" fill="white">
+                            <use xlink:href="#path-15"></use>
+                        </mask>
+                        <use id="路径-247" fill="#D3F0E0" xlink:href="#path-15"></use>
+                        <path d="M60.5426357,11.3128799 C60.8171154,11.2826219 61.064154,11.4806027 61.094412,11.7550823 C61.12467,12.0295619 60.9266892,12.2766006 60.6522096,12.3068585 C39.729997,14.6132741 18.9462607,31.6462845 -1.67037213,63.4563407 C-1.8205596,63.6880697 -2.13016408,63.7541722 -2.36189309,63.6039847 C-2.5936221,63.4537972 -2.65972458,63.1441927 -2.50953711,62.9124637 C18.2541689,30.8754836 39.2620175,13.6588053 60.5426357,11.3128799 Z" id="路径-253" fill="#9FC8B1" fill-rule="nonzero" mask="url(#mask-16)"></path>
+                        <path d="M77,14.004188 C76.1582967,19.0507483 74.4123575,22.6778642 71.7621824,24.8855357 C67.7869198,28.1970428 61.7621824,29.629188 56.5004637,30.6914365 C51.2387449,31.753685 28.3095457,36.4578462 17.2245848,45.7839732 L39.1176512,38.3640513 L70.2081638,30.6914365 L79.9838621,20.4621958 L77,14.004188 Z" id="路径-357" fill="#C4E0D1" mask="url(#mask-16)"></path>
+                    </g>
+                    <g id="编组-105" transform="translate(17.048956, 30.887531)">
+                        <mask id="mask-18" fill="white">
+                            <use xlink:href="#path-17"></use>
+                        </mask>
+                        <use id="路径-248" fill="#EAFFF3" xlink:href="#path-17"></use>
+                        <path d="M69.3618111,119.305105 C64.1514723,95.5149533 58.5592828,78.7727476 52.5852425,69.0784883 C43.6241821,54.5370993 32.2521675,45.1631445 20.9273327,40.7089848 C9.60249781,36.2548251 0.370054887,30.3143707 -0.833749667,21.5991494 C-2.03755422,12.8839281 3.30184276,3.89650161 14.9982131,2.35985332 C26.6945835,0.823205037 38.680528,3.89650161 49.3232751,17.6556441 C59.9660221,31.4147866 70.7898492,73.0503233 70.7898492,89.5111312 C70.7898492,100.485003 70.3138365,110.416328 69.3618111,119.305105 Z" id="路径-248" fill="#D3F0E0" mask="url(#mask-18)"></path>
+                        <path d="M23.8031025,13.6524169 C23.9507563,13.4197035 24.2596222,13.3495935 24.4929738,13.4972473 C53.619825,31.9273315 69.2261583,67.7464847 71.3453163,120.899853 C71.3559107,121.175776 71.1411468,121.408372 70.8652236,121.419775 C70.5893003,121.43037 70.3567042,121.215606 70.3457056,120.939683 C68.238827,68.0842278 52.7653698,32.5700476 23.9582721,14.3422882 C23.7249205,14.1946344 23.6554487,13.8857685 23.8031025,13.6524169 Z" id="路径-254" fill="#9FC8B1" fill-rule="nonzero" mask="url(#mask-18)"></path>
+                        <path d="M2.10955328,9.39371881 C-0.468124531,21.2459323 4.83890245,29.8251967 18.0306342,35.1315118 C37.8182319,43.0909844 45.0262397,50.6209933 54.8953803,65.9239922 C61.4748074,76.1259915 66.5415392,93.4846608 70.0955756,118 L-6.37610406,29.466961 L2.10955328,9.39371881 Z" id="路径-354" fill="#C4E0D1" mask="url(#mask-18)"></path>
+                    </g>
+                    <g id="编组-104" transform="translate(48.402642, -0.000000)">
+                        <mask id="mask-20" fill="white">
+                            <use xlink:href="#path-19"></use>
+                        </mask>
+                        <use id="路径-249" fill="#EAFFF3" xlink:href="#path-19"></use>
+                        <path d="M38.4361627,109.727577 C40.2080966,71.0333394 39.2052946,44.753324 35.4277569,30.8875312 C29.7614504,10.088842 20.8541813,-1.27827958 9.37287413,0.114578571 C-2.10843299,1.50743672 -4.5866861,11.539269 0.542720885,19.2423116 C5.67212787,26.9453541 20.1964111,48.5363293 25.3543068,61.4631547 C28.7929039,70.0810384 33.1535225,86.1691793 38.4361627,109.727577 Z" id="路径-249" fill="#D3F0E0" mask="url(#mask-20)" transform="translate(18.642412, 54.863789) rotate(-2.000000) translate(-18.642412, -54.863789) "></path>
+                        <path d="M1.96015082,4.87988281 C0.585845275,10.6699219 1.44620769,15.6948242 4.54123807,19.9545898 C9.18378363,26.3442383 28.9997016,54.6122295 32.8727485,77.5431753 L3.69550238,25.6259766 L-2.5784234,12.7954102 L1.96015082,4.87988281 Z" id="路径-356" fill="#C4E0D1" mask="url(#mask-20)"></path>
+                    </g>
+                    <g id="编组-106" transform="translate(-0.000000, 140.484501)">
+                        <mask id="mask-22" fill="white">
+                            <use xlink:href="#path-21"></use>
+                        </mask>
+                        <use id="路径-250" fill="#EAFFF3" xlink:href="#path-21"></use>
+                        <path d="M86.8630745,45.7959111 C72.5806324,25.5140129 56.8667378,12.125403 39.7213908,5.6300812 C14.0033702,-4.11290144 -7.10542736e-15,7.90110838 -7.10542736e-15,16.52167 C-7.10542736e-15,25.1422316 6.80949202,30.0268155 17.0489556,30.0268155 C27.2884192,30.0268155 43.7234658,28.0070237 58.8280258,36.5737997 C68.8977326,42.2849837 79.1842128,51.927944 89.6874666,65.5026805 L86.8630745,45.7959111 Z" id="路径-250" fill="#D3F0E0" mask="url(#mask-22)"></path>
+                        <path d="M1.1501319,20.1112023 C3.60224814,22.8815414 6.77648803,24.6116846 10.6728516,25.301632 C16.5173969,26.3365529 23.2104492,26.0726281 29.4458008,26.0726281 C47.5162559,26.0726281 66.1025391,30.4051476 89.6874666,63.5026805 L73.4389648,56.501339 L2.37988281,27.6380577 L1.1501319,20.1112023 Z" id="路径-355" fill="#C4E0D1" mask="url(#mask-22)"></path>
+                    </g>
+                    <path d="M63.5963011,11.6405216 C63.8094921,11.4650103 64.1245976,11.4955556 64.3001089,11.7087466 C74.8415791,24.513311 82.3205992,48.9543952 86.3331527,79.7677903 L86.4536838,80.7034746 C89.7765293,106.781999 90.2171879,135.285754 87.8968469,153.563607 L87.8038207,154.279761 C84.7801018,177.032488 87.8036828,199.576221 96.8773487,221.919195 C96.9812512,222.175044 96.858074,222.466681 96.6022247,222.570583 C96.3463753,222.674486 96.0547388,222.551308 95.9508363,222.295459 C86.945111,200.119782 83.8552445,177.733575 86.6839023,155.144723 L86.812536,154.148024 C89.2247883,135.99643 88.8179579,107.170593 85.4617038,80.8298697 L85.2198735,78.9727728 C81.2011719,48.7675619 73.814123,24.8386348 63.5280761,12.3443293 C63.3525648,12.1311384 63.3831102,11.8160329 63.5963011,11.6405216 Z" id="路径-245" fill="#9FC8B1" fill-rule="nonzero"></path>
+                    <path d="M19.210295,152.513695 C50.2079907,155.741553 73.2591803,169.485692 88.3189956,193.730479 C88.4647019,193.965052 88.3926616,194.273329 88.1580891,194.419035 C87.9235165,194.564741 87.6152396,194.492701 87.4695333,194.258128 C72.5772256,170.283012 49.8045783,156.704952 19.1067228,153.508317 C18.8320656,153.479717 18.6325973,153.233878 18.6611979,152.95922 C18.6866207,152.715081 18.8836888,152.530349 19.1200684,152.512399 L19.210295,152.513695 Z" id="路径-252" fill="#9FC8B1" fill-rule="nonzero"></path>
+                </g>
+                <g id="编组-76" transform="translate(570.109175, 160.235303)">
+                    <mask id="mask-24" fill="white">
+                        <use xlink:href="#path-23"></use>
+                    </mask>
+                    <use id="椭圆形" fill="#FFFFFF" xlink:href="#path-23"></use>
+                    <polygon id="路径-306" fill="#CCDEFF" mask="url(#mask-24)" points="42 44.0199101 77.9801299 18.2088648 90.719119 40.5 84 59.9151825"></polygon>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

File diff suppressed because it is too large
+ 0 - 0
src/assets/image/action.svg


+ 1 - 0
src/assets/image/attach.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g fill="none"><path fill="url(#fluentColorDocumentFolder160)" d="M5 5.5A1.5 1.5 0 0 1 6.5 4h5A1.5 1.5 0 0 1 13 5.5v5a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 5 10.5z"/><path fill="url(#fluentColorDocumentFolder161)" d="M5 5.5A1.5 1.5 0 0 1 6.5 4h5A1.5 1.5 0 0 1 13 5.5v5a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 5 10.5z"/><path fill="url(#fluentColorDocumentFolder163)" d="M3 3.5A1.5 1.5 0 0 1 4.5 2h5A1.5 1.5 0 0 1 11 3.5v7A1.5 1.5 0 0 1 9.5 12h-5A1.5 1.5 0 0 1 3 10.5z"/><path fill="url(#fluentColorDocumentFolder162)" d="M3.5 5A1.5 1.5 0 0 0 2 6.5V12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-.5a1.5 1.5 0 0 0-1.5-1.5h-1.586a.5.5 0 0 1-.353-.146L6.146 5.439A1.5 1.5 0 0 0 5.086 5z"/><defs><linearGradient id="fluentColorDocumentFolder160" x1="14.2" x2="15.247" y1="13.539" y2="5.069" gradientUnits="userSpaceOnUse"><stop stop-color="#bb45ea"/><stop offset="1" stop-color="#9c6cfe"/></linearGradient><linearGradient id="fluentColorDocumentFolder161" x1="13" x2="11" y1="6.769" y2="6.769" gradientUnits="userSpaceOnUse"><stop offset=".338" stop-color="#5750e2" stop-opacity="0"/><stop offset="1" stop-color="#5750e2"/></linearGradient><linearGradient id="fluentColorDocumentFolder162" x1="4.571" x2="4.571" y1="5" y2="17.273" gradientUnits="userSpaceOnUse"><stop offset=".241" stop-color="#ffd638"/><stop offset=".637" stop-color="#fab500"/><stop offset=".985" stop-color="#ca6407"/></linearGradient><radialGradient id="fluentColorDocumentFolder163" cx="0" cy="0" r="1" gradientTransform="matrix(5.2 -7.66667 11.90405 8.07405 5.4 10)" gradientUnits="userSpaceOnUse"><stop offset=".228" stop-color="#2764e7"/><stop offset=".685" stop-color="#5cd1ff"/><stop offset="1" stop-color="#6ce0ff"/></radialGradient></defs></g></svg>

+ 1 - 0
src/assets/image/login.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g fill="none"><path fill="url(#fluentColorPoll160)" d="M6 3a2 2 0 1 1 4 0v10a2 2 0 1 1-4 0z"/><path fill="url(#fluentColorPoll161)" d="M13 5a2 2 0 0 0-2 2v6a2 2 0 1 0 4 0V7a2 2 0 0 0-2-2"/><path fill="url(#fluentColorPoll162)" d="M3 7a2 2 0 0 0-2 2v4a2 2 0 1 0 4 0V9a2 2 0 0 0-2-2"/><defs><linearGradient id="fluentColorPoll160" x1="9.667" x2="7.529" y1="12.433" y2=".854" gradientUnits="userSpaceOnUse"><stop stop-color="#6d37cd"/><stop offset="1" stop-color="#ea71ef"/></linearGradient><linearGradient id="fluentColorPoll161" x1="14.667" x2="13.558" y1="13.167" y2="4.76" gradientUnits="userSpaceOnUse"><stop stop-color="#e23cb4"/><stop offset="1" stop-color="#ea71ef"/></linearGradient><linearGradient id="fluentColorPoll162" x1="1.5" x2="9.148" y1="7.333" y2="11.857" gradientUnits="userSpaceOnUse"><stop stop-color="#36dff1"/><stop offset="1" stop-color="#0078d4"/></linearGradient></defs></g></svg>

File diff suppressed because it is too large
+ 0 - 0
src/assets/image/user.svg


File diff suppressed because it is too large
+ 0 - 0
src/assets/login_picture.svg


BIN
src/assets/userBanner.jpg


+ 130 - 0
src/components/game-permission/index.vue

@@ -0,0 +1,130 @@
+<template>
+  <div>
+    <a-radio-group type="button" v-model="selectType">
+      <a-radio value="*">全部游戏</a-radio>
+      <a-radio value="0">指定游戏</a-radio>
+    </a-radio-group>
+    <a-tree
+      v-if="selectType === '0'"
+      v-model:checkedKeys="checkedKeys"
+      checkable
+      checked-strategy="child"
+      multiple
+      :data="treeData"
+      :field-names="{ children: 'children', title: 'title', key: 'key' }"
+      @check="handleCheck"
+    />
+  </div>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+
+const selectType = ref("*");
+const checkedKeys = ref([]);
+const gameList = ref("");
+const data = ref([]);
+const allGameIds = ref([]);
+
+// 将原始数据转换为树形数据结构
+const treeData = computed(() => {
+  console.log("原始数据:", data.value);
+  const result = data.value.map((item) => ({
+    key: item.id || item.name, // Tree组件的唯一标识
+    title: item.name, // Tree组件显示的文本
+    children: item.children
+      ? item.children.map((child) => ({
+          key: child.id || `${item.id || item.name}_${child.name}`, // 确保子项有唯一key
+          title: child.name, // Tree组件显示的文本
+        }))
+      : [],
+  }));
+  console.log("转换后的树形数据:", result);
+  return result;
+});
+
+const init = (allGameData, checkedGameList) => {
+  data.value = [];
+  gameList.value = checkedGameList;
+  checkedKeys.value =
+    checkedGameList === "*"
+      ? ["*"]
+      : checkedGameList?.split(",").map(Number) || [];
+  selectType.value = checkedGameList === "*" ? "*" : "0";
+
+  transformAllGamedata(allGameData);
+};
+
+const transformAllGamedata = (allGameData) => {
+  let resGameIds = [];
+
+  allGameData.forEach((item) => {
+    item.checked =
+      checkedKeys.value.includes(item.id) || checkedKeys.value[0] === "*";
+    if (item.children && Array.isArray(item.children)) {
+      item.children.forEach((child) => {
+        if (
+          checkedKeys.value.includes(child.id) ||
+          checkedKeys.value[0] === "*"
+        ) {
+          child.checked = true;
+          resGameIds.push(child.id);
+        } else {
+          child.checked = false;
+        }
+      });
+    }
+  });
+  data.value = allGameData;
+  allGameIds.value = resGameIds;
+  // 初始化选中的keys
+  initCheckedKeys();
+};
+
+// 初始化选中状态
+const initCheckedKeys = () => {
+  const keys = [];
+  data.value.forEach((parent) => {
+    // if (parent.checked) {
+    //   keys.push(parent.id || parent.name);
+    // }
+    if (parent.children && Array.isArray(parent.children)) {
+      parent.children.forEach((child) => {
+        if (child.checked) {
+          keys.push(child.id || `${parent.id || parent.name}_${child.name}`);
+        }
+      });
+    }
+  });
+  checkedKeys.value = keys;
+};
+
+// 处理树节点选中状态变化
+const handleCheck = (checkedKeysValue, e) => {
+  console.log("选中状态变化:", checkedKeysValue);
+  checkedKeys.value = checkedKeysValue;
+  // 同步更新原始数据的选中状态
+  data.value.forEach((parent) => {
+    const parentKey = parent.id || parent.name;
+    parent.checked = checkedKeys.value.includes(parentKey);
+
+    if (parent.children && Array.isArray(parent.children)) {
+      parent.children.forEach((child) => {
+        const childKey =
+          child.id || `${parent.id || parent.name}_${child.name}`;
+        child.checked = checkedKeys.value.includes(childKey);
+      });
+    }
+  });
+};
+
+const getGamePermissionData = async () => {
+  return {
+    selectType: selectType.value,
+    checkedKeys: checkedKeys.value,
+    gameList: gameList.value,
+  };
+};
+
+defineExpose({ init, getGamePermissionData });
+</script>

+ 74 - 0
src/components/index.js

@@ -0,0 +1,74 @@
+import { use } from 'echarts/core'
+import { CanvasRenderer } from 'echarts/renderers'
+import {
+  BarChart,
+  LineChart,
+  PieChart,
+  RadarChart,
+  GaugeChart
+} from 'echarts/charts'
+import {
+  GridComponent,
+  TooltipComponent,
+  LegendComponent,
+  DataZoomComponent,
+  GraphicComponent
+} from 'echarts/components'
+
+import MaWangEditor from './ma-wangEditor/index.vue'
+import MaColorPicker from './ma-colorPicker/index.vue'
+import MaCityLinkage from './ma-cityLinkage/index.vue'
+
+import SaChart from './sa-chart/index.vue'
+import SaCheckbox from './sa-checkbox/index.vue'
+import SaRadio from './sa-radio/index.vue'
+import SaSelect from './sa-select/index.vue'
+import SaSwitch from './sa-switch/index.vue'
+import SaTable from './sa-table/index.vue'
+import SaTreeSlider from './sa-treeSlider/index.vue'
+import SaResource from './sa-resource/index.vue'
+import SaResourceButton from './sa-resource/button.vue'
+import SaDict from './sa-dict/index.vue'
+import SaUser from './sa-user/index.vue'
+import SaUploadImage from './sa-upload-image/index.vue'
+import SaUploadFile from './sa-upload-file/index.vue'
+import SaIcon from './sa-icon/index.vue'
+import SaIconPicker from './sa-icon-picker/index.vue'
+
+use([
+  CanvasRenderer,
+  BarChart,
+  LineChart,
+  PieChart,
+  RadarChart,
+  GaugeChart,
+  GridComponent,
+  TooltipComponent,
+  LegendComponent,
+  DataZoomComponent,
+  GraphicComponent
+])
+
+export default {
+  install(Vue) {
+    Vue.component('MaWangEditor', MaWangEditor)
+    Vue.component('MaColorPicker', MaColorPicker)
+    Vue.component('MaCityLinkage', MaCityLinkage)
+
+    Vue.component('SaChart', SaChart)
+    Vue.component('SaCheckbox', SaCheckbox)
+    Vue.component('SaRadio', SaRadio)
+    Vue.component('SaSelect', SaSelect)
+    Vue.component('SaSwitch', SaSwitch)
+    Vue.component('SaTable', SaTable)
+    Vue.component('SaTreeSlider', SaTreeSlider)
+    Vue.component('SaResource', SaResource)
+    Vue.component('SaResourceButton', SaResourceButton)
+    Vue.component('SaDict', SaDict)
+    Vue.component('SaUser', SaUser)
+    Vue.component('SaUploadImage', SaUploadImage)
+    Vue.component('SaUploadFile', SaUploadFile)
+    Vue.component('SaIcon', SaIcon)
+    Vue.component('SaIconPicker', SaIconPicker)
+  }
+}

+ 179 - 0
src/components/ma-cityLinkage/index.vue

@@ -0,0 +1,179 @@
+<template>
+  <a-cascader
+    v-if="props.type === 'cascader'"
+    v-model="val"
+    :field-names="
+      props.mode == 'name'
+        ? { value: 'name', label: 'name' }
+        : { value: 'code', label: 'name' }
+    "
+    :options="jsonData"
+    allow-search
+    check-strictly
+    expand-trigger="hover"
+    path-mode
+    placeholder="请选择省市区"
+  />
+  <a-space v-else>
+    <a-select
+      v-model="selectData.province"
+      :field-names="
+        props.mode == 'name'
+          ? { value: 'name', label: 'name' }
+          : { value: 'code', label: 'name' }
+      "
+      :options="province"
+      :style="{ width: '220px' }"
+      allow-clear
+      allow-search
+      placeholder="请选择省/直辖市/自治区"
+      @change="provinceChange"
+      @clear="
+        () => {
+          selectData.city = [];
+          selectData.area = [];
+          selectData.province = [];
+          selectData.city = [];
+          selectData.area = [];
+          province.value = [];
+        }
+      "
+    />
+    <a-select
+      v-model="selectData.city"
+      :field-names="
+        props.mode == 'name'
+          ? { value: 'name', label: 'name' }
+          : { value: 'code', label: 'name' }
+      "
+      :options="city"
+      :style="{ width: '220px' }"
+      allow-clear
+      allow-search
+      placeholder="请选择地级市/市辖区"
+      @change="cityChange"
+      @clear="
+        () => {
+          selectData.city = [];
+          selectData.area = [];
+          selectData.city = [];
+          selectData.area = [];
+        }
+      "
+    />
+    <a-select
+      v-model="selectData.area"
+      :field-names="
+        props.mode == 'name'
+          ? { value: 'name', label: 'name' }
+          : { value: 'code', label: 'name' }
+      "
+      :options="area"
+      :style="{ width: '220px' }"
+      allow-clear
+      allow-search
+      placeholder="请选择区县"
+      @clear="
+        () => {
+          selectData.area = [];
+          selectData.area = [];
+        }
+      "
+    />
+  </a-space>
+</template>
+
+<script setup>
+import jsonData from "./lib/city.json";
+import { ref, watch } from "vue";
+import { isObject } from "lodash";
+
+const val = ref();
+const selectData = ref({ province: [], city: [], area: [] });
+const province = ref([]);
+const city = ref([]);
+const area = ref([]);
+
+const emit = defineEmits(["update:modelValue"]);
+const props = defineProps({
+  modelValue: [Number, String, Object],
+  type: { type: String, default: "select" },
+  mode: { type: String, default: "name" },
+});
+
+if (props.type === "select") {
+  province.value = jsonData.map((item) => {
+    return { code: item.code, name: item.name };
+  });
+}
+
+const provinceChange = (val, clear = true) => {
+  if (clear) {
+    selectData.value.city = [];
+    selectData.value.area = [];
+    area.value = [];
+    city.value = [];
+  }
+  jsonData.map((item) => {
+    if (props.mode == "name" && val == item.name) {
+      city.value = item.children;
+    }
+    if (props.mode == "code" && val == item.code) {
+      city.value = item.children;
+    }
+  });
+};
+
+const cityChange = (val, clear = true) => {
+  if (clear) {
+    selectData.value.area = [];
+    area.value = [];
+  }
+  city.value.map((item) => {
+    if (props.mode == "name" && val == item.name) {
+      area.value = item.children;
+    }
+    if (props.mode == "code" && val == item.code) {
+      area.value = item.children;
+    }
+  });
+};
+
+const setSelectData = () => {
+  if (props.type === "select") {
+    if (val.value && isObject(val.value)) {
+      selectData.value.province = val.value.province ? val.value.province : "";
+      selectData.value.city = val.value.city ? val.value.city : "";
+      selectData.value.area = val.value.area ? val.value.area : "";
+      selectData.value.province && provinceChange(selectData.value.province, false);
+      selectData.value.city &&
+        selectData.value.province &&
+        cityChange(selectData.value.city, false);
+    }
+  }
+};
+
+val.value = props.modelValue;
+
+watch(
+  () => props.modelValue,
+  (vl) => {
+    val.value = vl;
+    setSelectData();
+  },
+  { deep: true }
+);
+
+watch(
+  () => val.value,
+  (vl) => emit("update:modelValue", vl)
+);
+
+watch(
+  () => selectData.value,
+  (vl) => emit("update:modelValue", vl),
+  { deep: true }
+);
+
+setSelectData();
+</script>

File diff suppressed because it is too large
+ 0 - 0
src/components/ma-cityLinkage/lib/city.json


+ 108 - 0
src/components/ma-codeEditor/index.vue

@@ -0,0 +1,108 @@
+<template>
+  <div class="editor" ref="dom" :style="'width: 100%; height: ' + props.height + 'px'"></div>
+</template>
+
+<script setup>
+import { onMounted, ref, watch, toRaw } from 'vue'
+import { useAppStore } from '@/store'
+import { formatJson } from '@/utils/common'
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
+import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'
+import 'monaco-editor/esm/vs/basic-languages/php/php.contribution'
+import 'monaco-editor/esm/vs/basic-languages/mysql/mysql.contribution'
+import 'monaco-editor/esm/vs/basic-languages/html/html.contribution'
+import 'monaco-editor/esm/vs/basic-languages/css/css.contribution'
+import 'monaco-editor/esm/vs/editor/contrib/find/browser/findController'
+
+const appStore = useAppStore()
+
+const props = defineProps({
+  modelValue: {
+    type: [String, Object, Array],
+    default: () => ''
+  },
+  defaultModelValue: {
+    type: String,
+    default: '',
+  },
+  valueType: {
+    type: String,
+    default: 'value'
+  },
+  miniMap: {
+    type: Boolean,
+    default: false
+  },
+  isBind: {
+    type: Boolean,
+    default: false
+  },
+  height: {
+    type: Number,
+    default: 400
+  },
+  language: {
+    type: String,
+    default: 'javascript'
+  },
+  readonly: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const options = {
+  tabSize: 4,
+  automaticLayout: true,
+  scrollBeyondLastLine: false,
+  language: props.language,
+  theme: appStore.mode === 'light' ? 'vs' : 'vs-dark',
+  autoIndent: true,
+  minimap: { enabled: props.miniMap },
+  readOnly: props.readonly,
+  folding: true,
+  acceptSuggestionOnCommitCharacter: true,
+  acceptSuggestionOnEnter: true,
+  contextmenu: true
+}
+
+const emit = defineEmits(['update:modelValue'])
+const dom = ref()
+
+let instance
+
+const initEditorValue = () => {
+  if (props.valueType === 'value' && typeof props.modelValue === 'string') {
+    instance.setValue(props.modelValue)
+  } else if (props.valueType === 'value' && props.modelValue?._onWillDispose === undefined) {
+    instance.setValue(formatJson(props.modelValue))
+  } else if (props.modelValue){
+    instance.setModel(toRaw(props.modelValue))
+  } else {
+    instance.setModel(monaco.editor.createModel(props.defaultModelValue, props.language))
+  }
+}
+
+watch( () => props.modelValue, () => initEditorValue() )
+
+onMounted(() => {
+  instance = monaco.editor.create(dom.value, options)
+  initEditorValue()
+
+  instance.onDidBlurEditorText(() => {
+    emit('update:modelValue', toRaw(props.valueType === 'value' ? instance.getValue() : instance.getModel()))
+  })
+})
+
+const getInstance = () => instance
+
+defineExpose({ getInstance, initEditorValue })
+</script>
+
+<style scoped lang="less">
+.editor {
+  border: 1px solid var(--color-border-2);
+  border-radius: 3px;
+  background: var(--color-bg-2);
+}
+</style>

+ 77 - 0
src/components/ma-colorPicker/index.vue

@@ -0,0 +1,77 @@
+<template>
+  <a-input-group class="w-full">
+    <a-trigger position="bottom" trigger="click" auto-fit-position :unmount-on-close="false">
+      <a-button type="primary">选择颜色</a-button>
+      <template #content>
+        <ColorPicker
+          theme="dark"
+          :color="val"
+          :sucker-hide="true"
+          :colors-default="defaultColorList"
+          @changeColor="selectColor"
+          style="width: 218px" />
+      </template>
+    </a-trigger>
+    <a-input v-model="val" :style="`color: ${val}`" :placeholder="props.placeholder"> </a-input>
+    <a-tooltip content="复制">
+      <a-button @click="copyColor"
+        ><template #icon><icon-copy class="cursor-pointer" /></template
+      ></a-button>
+    </a-tooltip>
+  </a-input-group>
+</template>
+<script setup>
+import { reactive, computed } from 'vue'
+import { ColorPicker } from 'vue-color-kit'
+import 'vue-color-kit/dist/vue-color-kit.css'
+import useClipboard from 'vue-clipboard3'
+import { Message } from '@arco-design/web-vue'
+
+const props = defineProps({
+  modelValue: String,
+  placeholder: { type: String, default: '请选择颜色' },
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const val = computed({
+  get() {
+    return props.modelValue
+  },
+  set(newVal) {
+    emit('update:modelValue', newVal)
+  },
+})
+
+const selectColor = (color) => {
+  val.value = color.hex
+}
+
+const copyColor = async () => {
+  try {
+    await useClipboard().toClipboard(val.value)
+    Message.success('复制成功')
+  } catch (e) {
+    Message.error('复制失败')
+  }
+}
+
+const defaultColorList = reactive([
+  '#165DFF',
+  '#F53F3F',
+  '#F77234',
+  '#F7BA1E',
+  '#00B42A',
+  '#14C9C9',
+  '#3491FA',
+  '#722ED1',
+  '#F5319D',
+  '#D91AD9',
+  '#34C759',
+  '#43a047',
+  '#7cb342',
+  '#c0ca33',
+  '#86909c',
+  '#6d4c41',
+])
+</script>

+ 110 - 0
src/components/ma-verifyCode/index.vue

@@ -0,0 +1,110 @@
+<script setup>
+import { ref, onMounted } from 'vue'
+import { Message } from '@arco-design/web-vue'
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
+const codeText = ref('')
+const verfiyCanvas = ref(null)
+const props = defineProps({
+  height: { type: Number, default: 36 },
+  width: { type: Number, default: 120 },
+  pool: { type: String, default: 'abcdefghjkmnpqrstuvwxyz23456789' },
+  size: { type: Number, default: 4 },
+  showError: { type: Boolean, default: true },
+})
+
+const checkResult = (verifyCode) => {
+  if (! verifyCode || verifyCode.length === 0) {
+    props.showError && Message.error(t('sys.verifyCode.notice'))
+    return false
+  }
+
+  if (verifyCode.toLowerCase() !== codeText.value.toLowerCase()) {
+    props.showError && Message.error(t('sys.verifyCode.error'))
+    generateCode()
+    return false
+  } else {
+    return true
+  }
+}
+
+const randomNum = (min, max) => {
+  return parseInt(Math.random() * (max - min) + min)
+}
+
+const randomColor = (min, max) => {
+  const r = randomNum(min, max)
+  const g = randomNum(min, max)
+  const b = randomNum(min, max)
+  return `rgb(${r},${g},${b})`
+}
+
+const generateCode = () => {
+  codeText.value = ''
+  const ctx = verfiyCanvas.value.getContext('2d')
+  ctx.fillStyle = randomColor(230, 255)
+  ctx.fillRect(0, 0, props.width, props.height)
+
+  for (let i = 0; i < props.size; i++) {
+    let currentText = '' + props.pool[randomNum(0, props.pool.length)]
+    codeText.value += currentText
+    ctx.font = '36px Simhei'
+    ctx.textAlign="center"
+    ctx.fillStyle = randomColor(80, 150)
+    ctx.fillText(currentText, (i + 1) * randomNum(20, 25), props.height / 2 + 13)
+  }
+
+  for (let i = 0; i < 5; i++) {
+    ctx.beginPath()
+    ctx.moveTo(randomNum(0, props.width), randomNum(0, props.height))
+    ctx.lineTo(randomNum(0, props.width), randomNum(0, props.height))
+    ctx.strokeStyle = randomColor(180, 230)
+    ctx.closePath()
+    ctx.stroke()
+  }
+
+  for (let i = 0; i < 40; i++) {
+    ctx.beginPath()
+    ctx.arc(randomNum(0, props.width), randomNum(0, props.height), 1, 0, 2 * Math.PI)
+    ctx.closePath()
+    ctx.fillStyle = randomColor(150, 200)
+    ctx.fill()
+  }
+
+  ctx.restore()
+  ctx.save()
+
+  return codeText
+}
+
+onMounted(() => {
+  generateCode()
+})
+
+const refresh = () => {
+  generateCode()
+}
+
+defineExpose({ checkResult, refresh })
+</script>
+
+<template>
+  <a-tooltip :content="t('sys.verifyCode.switch')">
+    <canvas
+      ref="verfiyCanvas"
+      class="canvas"
+      :width="props.width"
+      :height="props.height" @click="refresh"
+    />
+  </a-tooltip>
+</template>
+
+<style scoped lang="less">
+:deep(.arco-input-append){
+  padding: 0 !important;
+}
+.canvas {
+  cursor: pointer;
+}
+</style>

+ 194 - 0
src/components/ma-wangEditor/index.vue

@@ -0,0 +1,194 @@
+<template>
+  <!-- 组件外部的 form-item -->
+  <div style="z-index: 100; border: 1px solid #ccc; width: 100%">
+    <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />
+    <Editor
+      :style="{ height: props.height + 'px', overflowY: 'hidden' }"
+      v-model="content"
+      :defaultConfig="editorConfig"
+      :mode="props.mode"
+      @onCreated="handleCreated" />
+
+    <a-modal style="z-index: 1000" v-model:visible="resourceVisible" :render-to-body="false" :width="1080" :footer="false" draggable>
+      <template #title>资源选择器</template>
+      <sa-resource v-model="list" multiple ref="resource" returnType="url" />
+    </a-modal>
+  </div>
+</template>
+
+<script setup>
+import '@wangeditor/editor/dist/css/style.css'
+import { onBeforeUnmount, ref, shallowRef, watch, computed } from 'vue'
+import { Boot } from '@wangeditor/editor'
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+import { useAppStore } from '@/store'
+import commonApi from '@/api/common'
+import file2md5 from 'file2md5'
+import tool from '@/utils/tool'
+
+const resourceVisible = ref(false)
+const appStore = useAppStore()
+
+const props = defineProps({
+  modelValue: { type: String },
+  component: Object,
+  height: { type: Number, default: 300 },
+  mode: { type: String, default: 'default' },
+  customField: { type: String, default: undefined },
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+let registerWangEditorButtonFlag = appStore.appCurrentSetting.registerWangEditorButtonFlag
+
+const list = ref([])
+const resource = ref()
+
+let content = computed({
+  get() {
+    return props.modelValue
+  },
+  set(value) {
+    emit('update:modelValue', value)
+  },
+})
+
+watch(
+  () => content.value,
+  (vl) => emit('change', vl)
+)
+
+watch(
+  () => list.value,
+  (imgs) => {
+    let tmp = ''
+    imgs.map((img) => {
+      if (
+        img.indexOf('.jpg') > -1 ||
+        img.indexOf('.png') > -1 ||
+        img.indexOf('.bmp') > -1 ||
+        img.indexOf('.jpeg') > -1 ||
+        img.indexOf('.svg') > -1 ||
+        img.indexOf('.gif') > -1
+      ) {
+        const node = { type: 'image', src: img, href: '', alt: '', style: {}, children: [{ text: '' }] }
+        editorRef.value.insertNode(node)
+      }
+    })
+
+    resource.value.clearSelecteds()
+    resourceVisible.value = false
+  }
+)
+
+const editorRef = shallowRef()
+
+const toolbarConfig = {}
+toolbarConfig.excludeKeys = ['group-video', 'insertImage']
+
+class MyButtonMenu {
+  constructor() {
+    this.title = '资源选择器'
+    this.tag = 'button'
+  }
+
+  //  获取菜单执行时的 value ,用不到则返回空 字符串或 false
+  getValue(editor) {
+    return ''
+  }
+
+  //  // 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
+  isActive(editor) {
+    return false
+  }
+
+  // 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
+  isDisabled(editor) {
+    return false
+  }
+
+  // 点击菜单时触发的函数
+  exec(editor, value) {
+    editor.emit('click_menu')
+  }
+}
+
+const menu1Conf = {
+  key: 'menu1', // 定义 menu key :要保证唯一、不重复(重要)
+  factory() {
+    return new MyButtonMenu()
+  },
+}
+
+if (registerWangEditorButtonFlag === undefined || registerWangEditorButtonFlag === false) {
+  Boot.registerMenu(menu1Conf)
+
+  appStore.setRegisterWangEditorButtonFlag(true)
+}
+
+toolbarConfig.insertKeys = {
+  index: 1, // 插入的位置,基于当前的 toolbarKeys
+  keys: ['menu1'],
+}
+
+const editorConfig = {
+  placeholder: '请输入内容...',
+  MENU_CONF: {},
+  hoverbarKeys: {
+    // 在编辑器中,选中链接文本时,要弹出的菜单
+    link: {
+      menuKeys: [
+        // 默认的配置可以通过 `editor.getConfig().hoverbarKeys.image` 获取
+        'imageWidth30',
+        'imageWidth50',
+        'imageWidth100',
+        '|', // 分割符
+        'imageFloatNone', // 增加 '图片浮动' 菜单
+        'imageFloatLeft',
+        'imageFloatRight',
+        '|', // 分割符
+        'editImage',
+        'viewImageLink',
+        'deleteImage',
+      ],
+    },
+  },
+}
+
+editorConfig.MENU_CONF['uploadImage'] = {
+  async customUpload(file, insertFn) {
+    uploadRequest(file, 'image', 'uploadImage').then((res) => {
+      insertFn(tool.attachUrl(res.url))
+    })
+  },
+}
+
+const uploadRequest = async (file, type, method, requestData = {}) => {
+  const hash = await file2md5(file)
+  const dataForm = new FormData()
+  dataForm.append(type, file)
+  dataForm.append('isChunk', false)
+  dataForm.append('hash', hash)
+  for (let name in requestData) {
+    dataForm.append(name, requestData[name])
+  }
+  const response = await commonApi[method](dataForm)
+  return response.data
+}
+
+const handleCreated = (editor) => {
+  editorRef.value = editor
+
+  editorRef.value.on('click_menu', () => {
+    resourceVisible.value = true
+  })
+}
+
+onBeforeUnmount(() => {
+  const editor = editorRef.value
+  if (editor == null) return
+  editor.destroy()
+})
+</script>
+
+<style scoped></style>

+ 38 - 0
src/components/sa-chart/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <v-charts v-if="renderChart" :option="options" :autoresize="autoresize" :style="{ width, height }" />
+</template>
+
+<script setup>
+import { ref, nextTick } from 'vue'
+import VCharts from 'vue-echarts'
+import 'echarts/lib/component/title'
+
+const props = defineProps({
+  options: {
+    type: Object,
+    default() {
+      return {}
+    },
+  },
+  autoresize: {
+    type: Boolean,
+    default: true,
+  },
+  width: {
+    type: String,
+    default: '100%',
+  },
+  height: {
+    type: String,
+    default: '100%',
+  },
+})
+
+const renderChart = ref(false)
+
+nextTick(() => {
+  renderChart.value = true
+})
+</script>
+
+<style scoped lang="less"></style>

+ 41 - 0
src/components/sa-checkbox/index.vue

@@ -0,0 +1,41 @@
+<template>
+  <a-checkbox-group v-model="value" :direction="props.direction" :disabled="props.disabled" @change="handleChangeEvent($event)">
+    <template v-for="(item, index) in dictList[props.dict] ?? []">
+      <a-checkbox :value="item.value">{{ item.label }}</a-checkbox>
+    </template>
+  </a-checkbox-group>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import { useDictStore } from '@/store'
+
+const dictList = useDictStore().data
+const emit = defineEmits(['update:modelValue', 'change'])
+const value = ref()
+
+const props = defineProps({
+  modelValue: { type: Array, default: () => [] },
+  dict: { type: String, default: '' },
+  disabled: { type: Boolean, default: false },
+  direction: { type: String, default: 'horizontal' },
+})
+
+watch(
+  () => props.modelValue,
+  (vl) => {
+    value.value = vl
+  },
+  { immediate: true }
+)
+watch(
+  () => value.value,
+  (v) => {
+    emit('update:modelValue', value.value)
+  }
+)
+
+const handleChangeEvent = async (value) => {
+  emit('change', value)
+}
+</script>

+ 62 - 0
src/components/sa-dict/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <div>
+    <!-- 渲染 span -->
+    <span v-if="props.render === 'span'">
+      <template v-if="Array.isArray(value)">
+        {{
+          value.map((v) => tool.getLabel(v, props.options.length > 0 ? props.options : dictList[props.dict])).join(', ')
+        }}
+      </template>
+      <template v-else>
+        {{ tool.getLabel(value, props.options.length > 0 ? props.options : dictList[props.dict]) }}
+      </template>
+    </span>
+    <!-- 渲染 tag -->
+    <template v-if="props.render === 'tag'">
+      <template v-if="Array.isArray(value)">
+        <a-tag
+          v-for="(v, index) in value"
+          :key="index"
+          class="mr-2"
+          :color="
+            tool.getColor(v, props.options.length > 0 ? props.options : dictList[props.dict], props.colors || [])
+          ">
+          {{ tool.getLabel(v, props.options.length > 0 ? props.options : dictList[props.dict]) }}
+        </a-tag>
+      </template>
+      <a-tag
+        v-else-if="value !== ''"
+        :color="
+          tool.getColor(value, props.options.length > 0 ? props.options : dictList[props.dict], props.colors || [])
+        ">
+        {{ tool.getLabel(value, props.options.length > 0 ? props.options : dictList[props.dict]) }}
+      </a-tag>
+      <span v-else></span>
+    </template>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import tool from '@/utils/tool.js'
+import { useDictStore } from '@/store'
+
+const dictList = useDictStore().data
+const value = ref()
+
+const props = defineProps({
+  value: { type: [String, Number, Array] },
+  render: { type: String, default: 'tag' },
+  dict: { type: String, default: '' },
+  options: { type: Array, default: [] },
+  colors: { type: Array, default: [] },
+})
+
+watch(
+  () => props.value,
+  (vl) => {
+    value.value = vl
+  },
+  { immediate: true }
+)
+</script>

File diff suppressed because it is too large
+ 0 - 0
src/components/sa-icon-picker/iconify/bi.json


+ 121 - 0
src/components/sa-icon-picker/index.vue

@@ -0,0 +1,121 @@
+<template>
+    <div class="w-full">
+      <a-input-group class="w-full">
+        <a-input placeholder="请点击右侧按钮选择图标" v-if="props.preview" allow-clear v-model="currentIcon" />
+        <div class="icon-container" v-if="props.preview">
+          <sa-icon :icon="currentIcon" v-if="currentIcon" />
+        </div>
+        <a-button type="primary" @click="() => (visible = true)">选择图标</a-button>
+      </a-input-group>
+  
+      <a-modal v-model:visible="visible" width="800px" draggable :footer="false">
+        <template #title>选择图标</template>
+        <a-tabs class="tabs">
+          <a-tab-pane key="arco" title="Arco Design">
+            <ul class="arco">
+              <li v-for="icon in arcodesignIcons" :key="icon" @click="selectIcon(icon, 'arco')">
+                <component :is="icon" />
+              </li>
+            </ul>
+          </a-tab-pane>
+          <a-tab-pane key="bi" title="Bootstrap Icons">
+            <ul class="arco">
+              <li v-for="icon in biData" :key="icon" @click="selectIcon(icon, 'iconify')">
+                <Icon :icon="icon" />
+              </li>
+            </ul>
+          </a-tab-pane>
+        </a-tabs>
+      </a-modal>
+    </div>
+  </template>
+  
+  <script setup>
+  import { reactive, ref, computed } from 'vue'
+  import * as arcoIcons from '@arco-design/web-vue/es/icon'
+  import { Icon } from '@iconify/vue';
+  import biData from "./iconify/bi.json";
+  
+  const arcodesignIcons = reactive([])
+  const visible = ref(false)
+  
+  const props = defineProps({
+    modelValue: { type: String },
+    preview: { type: Boolean, default: true },
+  })
+  
+  const emit = defineEmits(['update:modelValue'])
+  
+  const currentIcon = computed({
+    get() {
+      return props.modelValue
+    },
+    set(value) {
+      // html标签名不能以数字开头
+      if ((/^[^\d].*/.test(value) && value) || !value) {
+        emit('update:modelValue', value)
+      }
+    },
+  })
+  
+  for (let icon in arcoIcons) {
+    arcodesignIcons.push(icon)
+  }
+  
+  arcodesignIcons.pop()
+  
+  const selectIcon = (icon, className) => {
+    currentIcon.value = icon
+    visible.value = false
+  }
+  
+  const handlerChange = (value) => {
+    selectIcon(value, '')
+  }
+  </script>
+  
+  <style scoped lang="less">
+  .icon-container {
+    width: 50px;
+    height: 32px;
+    background-color: var(--color-fill-1);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+  
+  .icon {
+    width: 1em;
+    fill: var(--color-text-1);
+  }
+  
+  .tabs {
+    ul {
+      display: flex;
+      flex-wrap: wrap;
+      padding-left: 7px;
+    }
+  
+    li {
+      border: 2px solid var(--color-fill-4);
+      margin-bottom: 10px;
+      margin-right: 6px;
+      padding: 5px;
+      cursor: pointer;
+    }
+  
+    li:hover,
+    li.active {
+      border: 2px solid rgb(var(--primary-6));
+    }
+  
+    & li svg {
+      width: 2.4em;
+      height: 2.4em;
+    }
+  }
+  :deep(.arco-select-option-content) {
+    width: 100%;
+  }
+  </style>
+  

+ 29 - 0
src/components/sa-icon/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <template v-if="value.indexOf(':') === -1">
+    <component :is="value" :size="props.size"></component>
+  </template>
+  <template v-else>
+    <Icon :icon="value" class="iconify-icon" :style="{ fontSize: props.size + 'px' }" />
+  </template>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import { Icon } from '@iconify/vue'
+const value = ref('')
+
+const props = defineProps({
+  icon: { type: String },
+  size: { type: Number, default: 24 },
+})
+
+watch(
+  () => props.icon,
+  (vl) => {
+    if (vl) {
+      value.value = vl
+    }
+  },
+  { immediate: true }
+)
+</script>

+ 55 - 0
src/components/sa-radio/index.vue

@@ -0,0 +1,55 @@
+<template>
+  <a-radio-group
+    v-model="value"
+    :direction="props.direction"
+    :type="props.type"
+    :disabled="props.disabled"
+    @change="handleChangeEvent($event)">
+    <a-radio v-if="props.allowNull" :value="props.nullValue">{{ props.nullLabel }}</a-radio>
+    <template v-for="(item, index) in dictList[props.dict] ?? []">
+      <a-radio :value="item.value">{{ item.label }}</a-radio>
+    </template>
+  </a-radio-group>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import { useDictStore } from '@/store'
+
+const dictList = useDictStore().data
+const emit = defineEmits(['update:modelValue', 'change'])
+const value = ref()
+
+const props = defineProps({
+  modelValue: { type: [String, Number] },
+  type: { type: String, default: 'radio' },
+  dict: { type: String, default: '' },
+  disabled: { type: Boolean, default: false },
+  direction: { type: String, default: 'horizontal' },
+  allowNull: { type: Boolean, default: false },
+  nullValue: { type: [String, Number], default: '' },
+  nullLabel: { type: String, default: '全部' },
+})
+
+watch(
+  () => props.modelValue,
+  (vl) => {
+    if (props.dict !== '') {
+      value.value = vl + ''
+    } else {
+      value.value = vl
+    }
+  },
+  { immediate: true }
+)
+watch(
+  () => value.value,
+  (v) => {
+    emit('update:modelValue', value.value)
+  }
+)
+
+const handleChangeEvent = async (value) => {
+  emit('change', value)
+}
+</script>

+ 82 - 0
src/components/sa-resource/button.vue

@@ -0,0 +1,82 @@
+<template>
+  <div class="inline-block">
+    <a-input-group class="w-full">
+      <a-trigger position="bottom" auto-fit-position :unmount-on-close="false">
+        <a-input v-model="inputValue" placeholder="请点击左侧按钮选择资源" readonly v-if="!props.multiple" />
+        <a-button v-else>预览已选</a-button>
+        <template #content>
+          <div class="trigger-content">
+            <a-empty v-if="list && list.length == 0" />
+            <a-image :src="inputValue" v-else-if="list && !isArray(list)" />
+            <div v-else>
+              <a-image-preview-group infinite>
+                <a-space>
+                  <a-image :src="item" v-for="(item, index) in list" width="100%" :key="index" />
+                </a-space>
+              </a-image-preview-group>
+            </div>
+          </div>
+        </template>
+      </a-trigger>
+      <a-button type="primary" @click="visible = true"><icon-experiment /> 资源选择器</a-button>
+    </a-input-group>
+    <a-modal v-model:visible="visible" :width="props.width" :footer="false" draggable>
+      <template #title>资源选择器</template>
+      <sa-resource v-model="list" :multiple="props.multiple" :only-data="props.onlyData" />
+    </a-modal>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref, watch } from 'vue'
+import { isArray } from 'lodash'
+
+const list = ref()
+const visible = ref(false)
+const inputValue = ref('')
+
+const emit = defineEmits(['update:modelValue'])
+
+const props = defineProps({
+  modelValue: { type: [String, Array] },
+  multiple: { type: Boolean, default: true },
+  onlyData: { type: Boolean, default: true },
+  width: { type: Number, default: 1080 },
+})
+
+watch(
+  () => props.modelValue,
+  (vl) => {
+    list.value = vl
+  },
+  { immediate: true }
+)
+
+watch(
+  () => list.value,
+  (vl) => {
+    emit('update:modelValue', list.value)
+    if (props.multiple) {
+      inputValue.value = isArray(list) ? list.value.join(',') : []
+    } else {
+      inputValue.value = list.value
+    }
+    visible.value = false
+  },
+  { immediate: true, deep: true }
+)
+</script>
+
+<style scoped>
+.trigger-content {
+  margin-top: 1px;
+  background: var(--color-fill-1);
+  border: 1px solid var(--color-fill-3);
+  width: 340px;
+  border-radius: var(--border-radius-medium);
+}
+:deep(.arco-space) {
+  display: block;
+  margin-bottom: 5px;
+}
+</style>

+ 253 - 0
src/components/sa-resource/index.vue

@@ -0,0 +1,253 @@
+<template>
+  <div class="w-full resource-container h-full lg:flex lg:justify-between rounded-sm">
+    <a-modal v-model:visible="openNetworkModal" ok-text="保存" :on-before-ok="saveNetworkImg" draggable>
+      <template #title>保存网络图片</template>
+      <a-input v-model="networkImg" class="mb-3" placeholder="请粘贴网络图片地址" allow-clear />
+      <a-image :src="networkImg" width="100%" style="min-height: 150px" />
+    </a-modal>
+    <div class="w-full lg:mt-2 flex flex-col">
+      <div class="lg:flex lg:justify-between">
+        <div class="flex">
+          <sa-upload-file v-model="uploadFile" multiple :show-list="false" />
+          <a-button class="ml-3" @click="openNetworkModal = true"> <icon-image /> 保存网络图片 </a-button>
+          <a-radio-group type="button" v-model="defaultKey" @change="handlerClick" class="ml-4">
+            <a-radio v-for="(item, index) in sliderData" :key="index" :value="item.value">{{ item.label }}</a-radio>
+          </a-radio-group>
+        </div>
+        <a-input v-model="filename" class="input-search lg:mt-0 mt-2" placeholder="文件名搜索" @press-enter="searchFile" />
+      </div>
+      <a-spin :loading="resourceLoading" tip="资源加载中" class="h-full">
+        <div class="resource-list mt-2" ref="rl" v-if="attachmentList && attachmentList.length > 0">
+          <div class="item rounded-sm" v-for="(item, index) in attachmentList" :key="item.hash" @click="selectFile(item, index)">
+            <img :src="!/^(http|https)/g.test(item.url) ? $tool.attachUrl(item.url) : item.url" v-if="item.mime_type.indexOf('image') !== -1" />
+            <div v-else class="text-3xl w-full h-full flex items-center justify-center">
+              {{ `.${item.suffix}` }}
+            </div>
+            <a-tooltip position="bottom">
+              <div class="file-name">
+                {{ item.origin_name }}
+              </div>
+              <template #content>
+                <div>存储名称:{{ item.object_name }}</div>
+                <div>存储目录:{{ item.storage_path }}</div>
+                <div>上传时间:{{ item.create_time }}</div>
+                <div>文件大小:{{ item.size_info }}</div>
+                <div>存储模式:{{ tool.getLabel(item.storage_mode, dictList['upload_mode']) }}</div>
+              </template>
+            </a-tooltip>
+          </div>
+        </div>
+        <a-empty v-else class="mt-10" />
+      </a-spin>
+      <div class="lg:flex lg:justify-between">
+        <a-pagination :total="pageInfo.total" v-model:current="pageInfo.currentPage" v-model:page-size="pageSize" @change="changePage" />
+        <a-button type="primary" @click="selectComplete" class="mt-3 lg:mt-0">确定</a-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, watch } from 'vue'
+import commonApi from '@/api/common'
+import tool from '@/utils/tool'
+import { Message } from '@arco-design/web-vue'
+import { useDictStore } from '@/store'
+
+const dictList = useDictStore().data
+
+const sliderData = ref([])
+const defaultKey = ref('all')
+const uploadFile = ref()
+const attachmentList = ref([])
+const openNetworkModal = ref(false)
+const networkImg = ref()
+const pageInfo = ref({
+  total: 1,
+  currentPage: 1,
+})
+const resourceLoading = ref(true)
+const pageSize = ref(21)
+const filename = ref()
+const selecteds = ref()
+const rl = ref()
+
+const emit = defineEmits(['update:modelValue'])
+
+const props = defineProps({
+  modelValue: { type: [String, Array] },
+  multiple: { type: Boolean, default: true },
+  onlyData: { type: Boolean, default: true },
+  returnType: { type: String, default: 'url' },
+})
+
+onMounted(async () => {
+  const treeData = dictList['attachment_type']
+  sliderData.value = [{ label: '所有', value: 'all' }, ...treeData]
+  await getAttachmentList({ page: 1 })
+
+  if (props.multiple) {
+    selecteds.value = []
+  }
+})
+
+const getAttachmentList = async (params = {}) => {
+  const requestParams = Object.assign(params, { limit: pageSize.value })
+  resourceLoading.value = true
+  const response = await commonApi.getResourceList(requestParams)
+  pageInfo.value = {
+    total: response?.data?.total ?? 0,
+    currentPage: response?.data?.current_page ?? 21,
+  }
+  attachmentList.value = response?.data?.data
+  resourceLoading.value = false
+}
+
+const handlerClick = async (val) => {
+  defaultKey.value = val
+  const type = val === 'all' ? undefined : val
+  await getAttachmentList({ mime_type: type })
+}
+
+const searchFile = async () => {
+  await getAttachmentList({ origin_name: filename.value })
+}
+
+const selectFile = (item, index) => {
+  const children = rl.value.children
+  const className = 'item rounded-sm'
+
+  if (!/^(http|https)/g.test(item.url)) {
+    item.url = tool.attachUrl(item.url)
+  }
+  if (children[index].className.indexOf('active') !== -1) {
+    children[index].className = className
+    if (props.multiple) {
+      selecteds.value.map((file, idx) => {
+        selecteds.value.splice(idx, 1)
+      })
+    } else {
+      selecteds.value = ''
+    }
+  } else {
+    if (props.multiple) {
+      children[index].className = className + ' active'
+      selecteds.value.push(props.onlyData ? item[props.returnType] : item)
+    } else {
+      if (document.querySelectorAll('.item.active').length < 1) {
+        children[index].className = className + ' active'
+        selecteds.value = props.onlyData ? item[props.returnType] : item
+      }
+    }
+  }
+}
+
+const clearSelecteds = () => {
+  if (rl.value && rl.value.children) {
+    const children = rl.value.children
+    for (let i = 0; i < children.length; i++) {
+      children[i].className = 'item rounded-sm'
+    }
+  }
+  if (props.multiple) {
+    selecteds.value = []
+  } else {
+    selecteds.value = undefined
+  }
+}
+
+const selectComplete = () => {
+  const files = props.multiple ? Object.assign([], selecteds.value) : selecteds.value
+  emit('update:modelValue', files)
+}
+
+const changePage = async (page) => {
+  await getAttachmentList({ page })
+}
+
+const saveNetworkImg = async (done) => {
+  if (!networkImg.value) {
+    Message.error('输入地址不能为空')
+    done(false)
+    return
+  }
+  const response = await commonApi.saveNetWorkImage({ url: networkImg.value })
+  if (response.code === 200) {
+    Message.success(response.message)
+    await getAttachmentList()
+    networkImg.value = undefined
+    done(true)
+  } else {
+    Message.error(response.message)
+    done(false)
+  }
+}
+
+watch(
+  () => uploadFile,
+  async () => await getAttachmentList(),
+  { deep: true }
+)
+
+defineExpose({ clearSelecteds })
+</script>
+
+<style scoped lang="less">
+.resource-container {
+  min-height: 560px;
+}
+.input-search {
+  width: 250px;
+}
+.resource-list {
+  display: flex;
+  width: 100%;
+  flex-wrap: wrap;
+  flex-direction: row;
+  align-content: center;
+  .item {
+    width: 138px;
+    height: 138px;
+    border: 2px solid var(--color-fill-1);
+    margin-right: 10px;
+    margin-bottom: 20px;
+    background-color: var(--color-fill-1);
+    cursor: pointer;
+    position: relative;
+    .file-name {
+      position: absolute;
+      bottom: 0px;
+      height: 23px;
+      width: 100%;
+      background: rgba(100, 100, 100, 0.3);
+      line-height: 23px;
+      font-size: 12px;
+      overflow: hidden;
+      padding: 0 10px;
+      text-align: center;
+      text-overflow: ellipsis;
+      color: #fff;
+    }
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+    }
+  }
+  .item:hover {
+    border: 2px solid rgb(var(--primary-6));
+  }
+  .item.active {
+    border: 2px solid rgb(var(--primary-6));
+  }
+  .item.active::after {
+    content: '';
+    position: absolute;
+    width: 134px;
+    height: 134px;
+    z-index: 2;
+    top: 0;
+    background: rgba(var(--primary-5), 0.2);
+  }
+}
+</style>

+ 55 - 0
src/components/sa-select/index.vue

@@ -0,0 +1,55 @@
+<template>
+  <a-select
+    v-model="value"
+    :size="props.size"
+    :options="dictList[props.dict] ?? []"
+    :placeholder="props.placeholder"
+    :style="props.style"
+    :disabled="props.disabled"
+    :allow-clear="props.allowClear"
+    @change="handleChangeEvent($event)">
+  </a-select>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import { useDictStore } from '@/store'
+
+const dictList = useDictStore().data
+const emit = defineEmits(['update:modelValue', 'change'])
+const value = ref()
+
+const props = defineProps({
+  modelValue: { type: [String, Number] },
+  fieldNames: { type: Object, default: { value: 'value', label: 'label' } },
+  size: { type: String, default: 'medium' },
+  style: { type: Object, default: {} },
+  dict: { type: String, default: '' },
+  placeholder: { type: String, default: '请选择' },
+  disabled: { type: Boolean, default: false },
+  allowClear: { type: Boolean, default: true },
+})
+
+watch(
+  () => props.modelValue,
+  (vl) => {
+    if (props.dict !== '') {
+      value.value = vl + ''
+    } else {
+      value.value = vl
+    }
+  },
+  { immediate: true }
+)
+watch(
+  () => value.value,
+  (v) => {
+    emit('update:modelValue', value.value)
+  }
+)
+
+const handleChangeEvent = async (val) => {
+  emit('update:modelValue', val)
+  emit('change', val)
+}
+</script>

+ 60 - 0
src/components/sa-switch/index.vue

@@ -0,0 +1,60 @@
+<template>
+  <a-switch
+    v-model="value"
+    :size="props.size"
+    :disabled="props.disabled"
+    :loading="props.loading"
+    :type="props.type"
+    :checked-value="props.checkedValue"
+    :unchecked-value="props.uncheckedValue"
+    :checked-color="props.checkedColor"
+    :unchecked-color="props.uncheckedColor"
+    @change="handleChangeEvent($event)">
+    <template #checked> {{ props.checkedText }} </template>
+    <template #unchecked> {{ props.uncheckedText }} </template>
+  </a-switch>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const emit = defineEmits(['update:modelValue', 'change'])
+const value = ref()
+
+const props = defineProps({
+  modelValue: { type: [String, Number, Boolean] },
+  size: { type: String, default: 'medium' },
+  type: { type: String, default: 'round' },
+  valType: { type: String, default: 'string' },
+  disabled: { type: Boolean, default: false },
+  loading: { type: Boolean, default: false },
+  checkedValue: { type: [String, Number, Boolean], default: '1' },
+  uncheckedValue: { type: [String, Number, Boolean], default: '2' },
+  checkedColor: { type: String, default: '' },
+  uncheckedColor: { type: String, default: '' },
+  checkedText: { type: String, default: '启用' },
+  uncheckedText: { type: String, default: '禁用' },
+})
+
+watch(
+  () => props.modelValue,
+  (vl) => {
+    if (props.valType === 'string') {
+      value.value = vl + ''
+    } else {
+      value.value = vl
+    }
+  },
+  { immediate: true }
+)
+watch(
+  () => value.value,
+  (v) => {
+    emit('update:modelValue', value.value)
+  }
+)
+
+const handleChangeEvent = async (value) => {
+  emit('change', value)
+}
+</script>

+ 130 - 0
src/components/sa-table/defaultOptions.js

@@ -0,0 +1,130 @@
+export default {
+  // 当前crud组件的 id,全局唯一,不指定则随机生成一个
+  id: undefined,
+  // 主键名称
+  pk: 'id',
+  // 请求api方法
+  api: () => {},
+  // 设置分页组件每页记录数
+  pageSizeOption: [10, 20, 30, 50, 100],
+  // 设置选择列
+  rowSelection: undefined,
+  // 是否显示边框
+  bordered: { wrapper: true, cell: false },
+  // 每页记录数
+  pageSize: 10,
+  // 默认展开所有行
+  expandAllRows: false,
+  // 是否显示总结行
+  showSummary: false,
+  // 斑马线
+  stripe: true,
+  // 表格大小
+  size: 'large',
+  // 是否显示展开/折叠按钮
+  isExpand: false,
+  // 是否显示工具栏
+  showTools: true,
+  // 页面布局方式,支持 normal(标准)和 fixed(固定)两种
+  pageLayout: 'fixed',
+  height: 0,
+  // 简洁模式
+  pageSimple: false,
+  // 显示排序
+  showSort: true,
+  // 显示搜索
+  showSearch: true,
+  // 搜索提交按钮文案
+  searchText: '搜索',
+  // 搜索重置按钮文案
+  resetText: '重置',
+  // 强制搜索一行显示
+  singleLine: false,
+
+  view: {
+    // 新增api
+    func: undefined,
+    // 显示新增按钮的权限
+    auth: [],
+    // 按钮文案
+    text: '查看',
+    // 是否显示
+    show: false
+  },
+
+  add: {
+    // 新增api
+    func: undefined,
+    // 显示新增按钮的权限
+    auth: [],
+    // 按钮文案
+    text: '新增',
+    // 是否显示
+    show: false
+  },
+  edit: {
+    // 编辑api
+    func: undefined,
+    // 显示编辑按钮的权限
+    auth: [],
+    // 按钮文案
+    text: '编辑',
+    // 是否显示
+    show: false
+  },
+  delete: {
+    // 删除api
+    func: undefined,
+    // 显示删除按钮的权限
+    auth: [],
+    // 按钮文案
+    text: '删除',
+    // 是否显示
+    show: false,
+    // 是否显示批量处理按钮
+    batch: true
+  },
+  import: {
+    // 导入url
+    url: undefined,
+    // 导入参数
+    params: {},
+    // 下载模板地址
+    templateUrl: undefined,
+    // 显示导入按钮的权限
+    auth: [],
+    // 按钮文案
+    text: '导入',
+    // 是否显示
+    show: false
+  },
+  export: {
+    // 导出url
+    url: undefined,
+    // 显示导出按钮的权限
+    auth: [],
+    // 按钮文案
+    text: '导出',
+    // 是否显示
+    show: false
+  },
+
+  // 列对齐方式
+  columnAlign: 'left',
+
+  // 是否显示索引列
+  showIndex: false,
+  // 索引列名称
+  indexLabel: '序号',
+  // 索引列宽度
+  indexColumnWidth: 70,
+  // 索引列固定方向,false 为不固定
+  indexColumnFixed: 'left',
+
+  // 是否显示操作列
+  operationColumn: true,
+  // 操作列宽度
+  operationColumnWidth: 190,
+  // 操作列名称
+  operationColumnText: '操作'
+}

+ 72 - 0
src/components/sa-table/import.vue

@@ -0,0 +1,72 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    :width="tool.getDevice() === 'mobile' ? '100%' : '600px'"
+    :footer="false"
+    @cancel="close"
+    draggable>
+    <template #title>导入</template>
+    <a-upload draggable :custom-request="upload" :show-file-list="false" accept=".xlsx,.xls">
+      <template #upload-button>
+        <div
+          style="background-color: var(--color-fill-2); border: 1px dashed var(--color-fill-4)"
+          class="rounded text-center p-7">
+          <div>
+            <icon-upload class="text-5xl text-gray-400" />
+            <div class="text-red-600 font-bold">导入Excel</div>
+            将文件拖到此处,或<span style="color: #3370ff">点击上传</span>,只能上传 xls/xlsx 文件
+          </div>
+        </div>
+      </template>
+    </a-upload>
+    <div class="mt-5 italic text-right"><a-link @click="sendDownload">下载Excel模板</a-link></div>
+  </a-modal>
+</template>
+
+<script setup>
+import { ref, inject } from 'vue'
+import commonApi from '@/api/common'
+import tool from '@/utils/tool'
+import { Message } from '@arco-design/web-vue'
+
+const visible = ref(false)
+
+const options = inject('options')
+
+const emit = defineEmits(['success'])
+
+const open = () => (visible.value = true)
+const close = () => (visible.value = false)
+
+const upload = (fileOption) => {
+  Message.info('文件上传导入中...')
+
+  const dataForm = new FormData()
+  dataForm.append('file', fileOption.fileItem.file)
+  if (options.import.params) {
+    Object.keys(options.import.params).forEach((key) => {
+      dataForm.append(key, options.import.params[key])
+    })
+  }
+  commonApi.importExcel(options.import.url, dataForm).then(async (res) => {
+    res.code === 200 && Message.success(res.message || '导入成功')
+    emit('success')
+    close()
+  })
+}
+
+const sendDownload = () => {
+  Message.info('请求服务器下载文件中...')
+  const url = options.import.templateUrl
+  if (/^(http|https)/g.test(url)) {
+    window.open(url)
+  } else {
+    commonApi.download(url).then((res) => {
+      tool.download(res)
+      Message.success('请求成功,文件开始下载')
+    })
+  }
+}
+
+defineExpose({ open, close })
+</script>

+ 768 - 0
src/components/sa-table/index.vue

@@ -0,0 +1,768 @@
+<template>
+  <a-layout-content class="flex flex-col lg:h-full relative w-full">
+    <a-card :bordered="false">
+      <div ref="crudHeaderRef">
+        <template v-if="showSearch">
+          <a-row v-if="tool.getDevice() === 'mobile'">
+            <a-col :xs="24" :sm="8">
+              <a-form :model="searchForm" ref="searchFormRef" :auto-label-width="true">
+                <a-row :gutter="10">
+                  <slot name="tableSearch"></slot>
+                </a-row>
+              </a-form>
+            </a-col>
+            <a-col :xs="24" :sm="8" :style="{ textAlign: 'right', marginBottom: '15px' }">
+              <a-space direction="horizontal" :size="20">
+                <a-button type="primary" @click="search">
+                  <template #icon>
+                    <icon-search />
+                  </template>
+                  {{ options.searchText || '搜索' }}
+                </a-button>
+                <a-button @click="resetSearch">
+                  <template #icon>
+                    <icon-refresh />
+                  </template>
+                  {{ options.resetText || '重置' }}
+                </a-button>
+              </a-space>
+            </a-col>
+          </a-row>
+          <a-row v-else>
+            <a-col :flex="1">
+              <a-form :model="searchForm" ref="searchFormRef" :auto-label-width="true">
+                <a-row :gutter="10">
+                  <slot name="tableSearch"></slot>
+                </a-row>
+              </a-form>
+            </a-col>
+            <a-divider v-if="!singleLine" style="height: 84px" direction="vertical" />
+            <a-col :flex="singleLine ? '185px' : '80px'" :style="{ textAlign: 'right' }">
+              <a-space :direction="!singleLine ? 'vertical' : 'horizontal'" :size="singleLine ? 10 : 20">
+                <a-button type="primary" @click="search">
+                  <template #icon>
+                    <icon-search />
+                  </template>
+                  {{ options.searchText || '搜索' }}
+                </a-button>
+                <a-button @click="resetSearch">
+                  <template #icon>
+                    <icon-refresh />
+                  </template>
+                  {{ options.resetText || '重置' }}
+                </a-button>
+              </a-space>
+            </a-col>
+          </a-row>
+          <a-divider style="margin-top: 0; margin-bottom: 15px" />
+        </template>
+      </div>
+      <div class="_crud-content">
+        <a-row style="margin-bottom: 10px" v-if="!options.pageSimple">
+          <a-col :xs="24" :sm="18">
+            <a-space :wrap="true">
+              <slot name="tableBeforeButtons"></slot>
+              <a-button type="primary" v-if="options.add.show" v-auth="options.add.auth || []" @click="addAction">
+                <template #icon> <icon-plus /> </template> {{ options.add.text || '新增' }}
+              </a-button>
+              <a-popconfirm
+                content="确定要删除数据吗?"
+                position="bottom"
+                @ok="deletesMultipleAction"
+                v-if="options.delete.show && options.rowSelection">
+                <a-button type="primary" status="danger" v-auth="options.delete.auth || []">
+                  <template #icon> <icon-delete /> </template> {{ options.delete.text || '删除' }}
+                </a-button>
+              </a-popconfirm>
+              <a-button v-if="options.import.show" v-auth="options.import.auth || []" @click="importAction">
+                <template #icon> <icon-upload /> </template> {{ options.import.text || '导入' }}
+              </a-button>
+              <a-button
+                v-if="options.export.show"
+                :loading="isExport"
+                v-auth="options.export.auth || []"
+                @click="exportAction">
+                <template #icon> <icon-download /> </template> {{ options.export.text || '导出' }}
+              </a-button>
+              <a-button type="secondary" @click="handlerExpand" v-if="options.isExpand">
+                <template #icon>
+                  <icon-expand v-if="!expandState" />
+                  <icon-shrink v-else />
+                </template>
+                {{ expandState ? ' 折叠' : ' 展开' }}
+              </a-button>
+              <slot name="tableAfterButtons"></slot>
+            </a-space>
+          </a-col>
+          <a-col
+            :xs="24"
+            :sm="6"
+            :style="{ textAlign: 'right', marginTop: tool.getDevice() === 'mobile' ? '15px' : '0' }">
+            <a-space v-if="options.showTools">
+              <slot name="tools"></slot>
+              <a-tooltip content="刷新表格" @click="refresh">
+                <a-button shape="circle"><icon-refresh /></a-button>
+              </a-tooltip>
+              <a-tooltip content="显隐搜索">
+                <a-button shape="circle" @click="searchChange"><icon-search /> </a-button>
+              </a-tooltip>
+              <a-tooltip content="打印表格"
+                ><a-button shape="circle" @click="printTable"><icon-printer /></a-button
+              ></a-tooltip>
+              <a-tooltip content="字段排序" v-if="options.showSort">
+                <a-popover trigger="click" position="br">
+                  <a-button shape="circle"><icon-sort /></a-button>
+                  <template #content>
+                    <div id="tableSetting">
+                      <div v-for="(item, index) in columns" :key="item.dataIndex" class="setting">
+                        <div style="margin-right: 4px">
+                          <icon-sort-ascending />
+                        </div>
+                        <div>
+                          <a-checkbox v-model="item.checked" @change="handleChange($event, item, index)"> </a-checkbox>
+                        </div>
+                        <div class="title">
+                          {{ item.title === '#' ? '序列号' : item.title }}
+                        </div>
+                      </div>
+                    </div>
+                  </template>
+                </a-popover>
+              </a-tooltip>
+            </a-space>
+          </a-col>
+        </a-row>
+        <div ref="crudContentRef">
+          <slot name="crudContent" v-bind="tableData">
+            <a-table
+              v-bind="$attrs"
+              ref="tableRef"
+              :key="options.pk"
+              :rowSelection="options.rowSelection ?? undefined"
+              :row-key="options.rowSelection?.key ?? options.pk"
+              :pagination="false"
+              :columns="columns"
+              :loading="loading"
+              :size="options.size"
+              :stripe="options.stripe"
+              :data="tableData.data"
+              :scroll="{ x: '100%', y: '100%' }"
+              :bordered="options.bordered"
+              :default-expand-all-rows="options.expandAllRows"
+              :summary="options.showSummary && __summary"
+              @selection-change="setSelecteds"
+              @sorter-change="handlerSort">
+              <template #columns>
+                <template v-for="(row, index) in columns" :key="index">
+                  <template v-if="row.children">
+                    <a-table-column :title="row.title">
+                      <template v-for="(rowChild, indexChild) in row.children">
+                        <a-table-column
+                          :title="rowChild.title"
+                          :data-index="rowChild.dataIndex"
+                          :width="rowChild.width"
+                          :min-width="rowChild.minWidth"
+                          :ellipsis="rowChild.ellipsis ?? true"
+                          :filterable="rowChild.filterable"
+                          :cell-class="rowChild.cellClass"
+                          :header-cell-class="rowChild.headerCellClass"
+                          :body-cell-class="rowChild.bodyCellClass"
+                          :summary-cell-class="rowChild.summaryCellClass"
+                          :cell-style="rowChild.cellStyle"
+                          :header-cell-style="rowChild.headerCellStyle"
+                          :body-cell-style="rowChild.bodyCellStyle"
+                          :summary-cell-style="rowChild.summaryCellStyle"
+                          :tooltip="rowChild.dataIndex === '__operation' ? false : rowChild.tooltip ?? true"
+                          :align="rowChild.align || options.columnAlign"
+                          :fixed="rowChild.fixed"
+                          :sortable="rowChild.sortable">
+                          <template #cell="{ record, column, rowIndex }">
+                            <template v-if="rowChild.dataIndex === '__index'">
+                              <span>{{ getIndex(rowIndex) }}</span>
+                            </template>
+                            <template v-else-if="rowChild.dataIndex === '__operation'">
+                              <a-scrollbar type="track" style="overflow: auto">
+                                <a-space size="mini">
+                                  <slot name="operationBeforeExtend" v-bind="{ record, column, rowIndex }"></slot>
+                                  <slot name="operationCell" v-bind="{ record, column, rowIndex }">
+                                    <a-link
+                                      v-if="options.edit.show"
+                                      v-auth="options.edit.auth || []"
+                                      type="primary"
+                                      @click="editAction(record)">
+                                      <icon-edit />{{ options.edit.text || '编辑' }}
+                                    </a-link>
+                                    <a-popconfirm
+                                      v-if="options.delete.show"
+                                      content="确定要删除该数据吗?"
+                                      position="bottom"
+                                      @ok="deleteAction(record)">
+                                      <a-link type="primary" v-auth="options.delete.auth || []">
+                                        <icon-delete /> {{ options.delete.text || '删除' }}
+                                      </a-link>
+                                    </a-popconfirm>
+                                  </slot>
+                                  <slot name="operationAfterExtend" v-bind="{ record, column, rowIndex }"></slot>
+                                </a-space>
+                              </a-scrollbar>
+                            </template>
+                            <slot
+                              v-else-if="rowChild.type === 'dict'"
+                              :name="rowChild.dataIndex"
+                              v-bind="{ record, column, rowIndex }">
+                              <sa-dict
+                                :value="record[rowChild.dataIndex]"
+                                :render="rowChild.render || 'tag'"
+                                :colors="rowChild.colors || []"
+                                :dict="rowChild.dict || []"
+                                :options="rowChild.options ?? []">
+                              </sa-dict>
+                            </slot>
+                            <template v-else-if="rowChild.type === 'image'">
+                              <a-avatar
+                                @click="imageSee(rowChild, record, rowChild.dataIndex)"
+                                :size="row.size || 64"
+                                shape="square">
+                                <img
+                                  :src="imageView(record[rowChild.dataIndex])"
+                                  style="object-fit: contain; cursor: pointer" />
+                              </a-avatar>
+                            </template>
+                            <slot v-else :name="rowChild.dataIndex" v-bind="{ record, column, rowIndex }">
+                              <span>{{ filterColumn(rowChild.dataIndex, record) }}</span>
+                            </slot>
+                          </template>
+                        </a-table-column>
+                      </template>
+                    </a-table-column>
+                  </template>
+                  <a-table-column
+                    v-else
+                    :title="row.title"
+                    :data-index="row.dataIndex"
+                    :width="row.width"
+                    :ellipsis="row.ellipsis ?? true"
+                    :filterable="row.filterable"
+                    :cell-class="row.cellClass"
+                    :header-cell-class="row.headerCellClass"
+                    :body-cell-class="row.bodyCellClass"
+                    :summary-cell-class="row.summaryCellClass"
+                    :cell-style="row.cellStyle"
+                    :header-cell-style="row.headerCellStyle"
+                    :body-cell-style="row.bodyCellStyle"
+                    :summary-cell-style="row.summaryCellStyle"
+                    :tooltip="row.dataIndex === '__operation' ? false : row.tooltip ?? true"
+                    :align="row.align || options.columnAlign"
+                    :fixed="row.fixed"
+                    :sortable="row.sortable">
+                    <template #cell="{ record, column, rowIndex }">
+                      <template v-if="row.dataIndex === '__index'">
+                        <span>{{ getIndex(rowIndex) }}</span>
+                      </template>
+                      <template v-else-if="row.dataIndex === '__operation'">
+                        <a-scrollbar type="track" style="overflow: auto">
+                          <a-space size="mini">
+                            <slot name="operationBeforeExtend" v-bind="{ record, column, rowIndex }"></slot>
+                            <slot name="operationCell" v-bind="{ record, column, rowIndex }">
+                              <a-link
+                                v-if="options.view.show"
+                                v-auth="options.view.auth || []"
+                                type="primary"
+                                @click="viewAction(record)">
+                                <icon-eye />{{ options.view.text || '查看' }}
+                              </a-link>
+                              <a-link
+                                v-if="options.edit.show"
+                                v-auth="options.edit.auth || []"
+                                type="primary"
+                                @click="editAction(record)">
+                                <icon-edit />{{ options.edit.text || '编辑' }}
+                              </a-link>
+                              <a-popconfirm
+                                v-if="options.delete.show"
+                                content="确定要删除该数据吗?"
+                                position="bottom"
+                                @ok="deleteAction(record)">
+                                <a-link type="primary" v-auth="options.delete.auth || []">
+                                  <icon-delete /> {{ options.delete.text || '删除' }}
+                                </a-link>
+                              </a-popconfirm>
+                            </slot>
+                            <slot name="operationAfterExtend" v-bind="{ record, column, rowIndex }"></slot>
+                          </a-space>
+                        </a-scrollbar>
+                      </template>
+                      <slot v-else-if="row.type === 'dict'" :name="row.dataIndex" v-bind="{ record, column, rowIndex }">
+                        <sa-dict
+                          :value="record[row.dataIndex]"
+                          :render="row.render || 'tag'"
+                          :colors="row.colors || []"
+                          :dict="row.dict || []"
+                          :options="row.options ?? []">
+                        </sa-dict>
+                      </slot>
+                      <template v-else-if="row.type === 'image'">
+                        <a-avatar @click="imageSee(row, record, row.dataIndex)" :size="row.size || 64" shape="square">
+                          <img :src="imageView(record[row.dataIndex])" style="object-fit: contain; cursor: pointer" />
+                        </a-avatar>
+                      </template>
+                      <slot v-else :name="row.dataIndex" v-bind="{ record, column, rowIndex }">
+                        <span>{{ filterColumn(row.dataIndex, record) }}</span>
+                      </slot>
+                    </template>
+                  </a-table-column>
+                </template>
+              </template>
+              <template #summary-cell="{ column, record, rowIndex }" v-if="options.showSummary">
+                <slot name="summaryCell" v-bind="{ record, column, rowIndex }">{{ record[column.dataIndex] }}</slot>
+              </template>
+            </a-table>
+          </slot>
+        </div>
+      </div>
+      <div class="mt-2 text-right" v-if="tableData.total > 0">
+        <a-pagination
+          :total="tableData.total"
+          show-total
+          show-jumper
+          show-page-size
+          :page-size-options="options.pageSizeOption"
+          @page-size-change="pageSizeChangeHandler"
+          @change="pageChangeHandler"
+          v-model:current="requestParams['page']"
+          :page-size="requestParams['limit']"
+          style="display: inline-flex" />
+      </div>
+    </a-card>
+
+    <sa-import ref="crudImportRef" @success="refresh" />
+
+    <a-image-preview-group
+      :srcList="imgUrl"
+      v-model:visible="imgVisible"
+      v-if="typeof imgUrl === 'object' && imgUrl !== null" />
+    <a-image-preview :src="imgUrl" v-model:visible="imgVisible" v-else />
+  </a-layout-content>
+</template>
+
+<script setup>
+import { ref, reactive, watch, provide, nextTick, onMounted, onUnmounted } from 'vue'
+import { isArray, isFunction, isObject, isUndefined, cloneDeep, get } from 'lodash'
+import defaultOptions from './defaultOptions'
+import tool from '@/utils/tool'
+import Print from '@/utils/print'
+import { request } from '@/utils/request'
+import { Message } from '@arco-design/web-vue'
+import { useDictStore } from '@/store'
+import SaImport from './import.vue'
+
+const props = defineProps({
+  // 表格数据
+  data: { type: [Function, Array], default: () => null },
+  // 表格设置
+  options: { type: Object, default: {} },
+  // 字段
+  columns: { type: Array, default: [] },
+  // 搜索表单
+  searchForm: { type: Object, default: () => {} },
+})
+
+const emit = defineEmits(['resetSearch'])
+
+const searchFormRef = ref()
+const crudHeaderRef = ref()
+const crudContentRef = ref()
+const headerHeight = ref(0)
+const crudImportRef = ref()
+const loading = ref(false)
+const showSearch = ref(true)
+const singleLine = ref(true)
+const currentApi = ref()
+const expandState = ref(false)
+const selecteds = ref([])
+const tableRef = ref()
+const isSort = ref(false)
+const isExport = ref(false)
+
+const imgVisible = ref(false)
+const imgUrl = ref(import.meta.env.VITE_APP_BASE + 'not-image.png')
+
+const options = ref(Object.assign(JSON.parse(JSON.stringify(defaultOptions)), props.options))
+
+const requestParams = ref({
+  page: 1,
+  limit: options.value.pageSize,
+})
+
+const searchForm = ref(props.searchForm)
+const columns = ref(props.columns)
+
+const tableData = reactive({
+  total: 0,
+  data: [],
+})
+
+provide('options', options.value)
+
+const filterColumn = (index, record) => {
+  return index.indexOf('.') > -1 ? get(record, index) : record[index]
+}
+
+const clearSelected = () => {
+  tableRef.value?.selectAll(false)
+}
+
+const setSelecteds = (key) => {
+  selecteds.value = key
+}
+
+const getTableData = () => {
+  return tableData.data
+}
+
+const getTableTotal = () => {
+  return tableData.total
+}
+
+const __summary = ({ data }) => {
+  if (options.value.showSummary && isArray(options.value.summary)) {
+    const summary = options.value.summary
+    let summaryData = {}
+    let summaryPrefixText = {}
+    let summarySuffixText = {}
+    let length = data.length || 0
+    summary.map((item) => {
+      if (item.action && item.action === 'text') {
+        summaryData[item.dataIndex] = item.text
+      } else {
+        summaryData[item.dataIndex] = 0
+        summaryPrefixText[item.dataIndex] = item?.prefixText ?? ''
+        summarySuffixText[item.dataIndex] = item?.suffixText ?? ''
+        data.map((record) => {
+          if (record[item.dataIndex]) {
+            if (item.action && item.action === 'sum') {
+              summaryData[item.dataIndex] += parseFloat(record[item.dataIndex])
+            }
+            if (item.action && item.action === 'avg') {
+              summaryData[item.dataIndex] += parseFloat(record[item.dataIndex]) / length
+            }
+          }
+        })
+      }
+    })
+
+    for (let i in summaryData) {
+      if (/^\d+(\.\d+)?$/.test(summaryData[i])) {
+        summaryData[i] = summaryPrefixText[i] + tool.groupSeparator(summaryData[i].toFixed(2)) + summarySuffixText[i]
+      }
+    }
+    return [summaryData]
+  }
+}
+
+const getIndex = (rowIndex) => {
+  const index = rowIndex + 1
+  if (requestParams.value['page'] === 1) {
+    return index
+  } else {
+    return (requestParams.value['page'] - 1) * requestParams.value['limit'] + index
+  }
+}
+
+// 页码变化
+const pageChangeHandler = async (currentPage) => {
+  requestParams.value['page'] = currentPage
+  await refresh()
+}
+
+// 每页数量变化
+const pageSizeChangeHandler = async (pageSize) => {
+  requestParams.value['page'] = 1
+  requestParams.value['limit'] = pageSize
+  await refresh()
+}
+
+// 搜索
+const search = async () => {
+  await refresh()
+}
+
+// 重置
+const resetSearch = async () => {
+  searchFormRef.value?.resetFields()
+  emit('resetSearch')
+  await refresh()
+}
+
+// 折叠/展开
+const handlerExpand = () => {
+  expandState.value = !expandState.value
+  expandState.value ? tableRef.value.expandAll(true) : tableRef.value.expandAll(false)
+}
+
+// 排序
+const handlerSort = async (name, type) => {
+  if (type) {
+    requestParams.value.orderBy = name
+    requestParams.value.orderType = type === 'ascend' ? 'asc' : 'desc'
+    isSort.value = true
+  } else {
+    requestParams.value.orderBy = undefined
+    requestParams.value.orderType = undefined
+    isSort.value = false
+  }
+  await refresh()
+}
+
+// 切换显示搜素框
+const searchChange = async () => {
+  showSearch.value = !showSearch.value
+  await nextTick(() => {
+    headerHeight.value = crudHeaderRef.value.offsetHeight
+    options.value.pageLayout === 'fixed' && settingFixedPage()
+  })
+}
+
+// 打印表格
+const printTable = () => {
+  new Print(crudContentRef.value)
+}
+
+// 排序
+const handleChange = (checked, column, index) => {
+  if (column.dataIndex == '__operation') {
+    return
+  }
+  if (checked) {
+    column.sortable = {
+      sortDirections: ['ascend', 'descend'],
+    }
+  } else {
+    column.sortable = undefined
+  }
+}
+
+// 初始化
+const init = async () => {
+  // 设置 组件id
+  if (isUndefined(options.value.id)) {
+    options.value.id = 'SaCrud_' + Math.floor(Math.random() * 100000 + Math.random() * 20000 + Math.random() * 5000)
+  }
+
+  // 设置序列号
+  if (options.value.showIndex && columns.value.length > 0 && columns.value[0].dataIndex !== '__index') {
+    columns.value.unshift({
+      title: options.value.indexLabel,
+      dataIndex: '__index',
+      width: options.value.indexColumnWidth,
+      fixed: options.value.indexColumnFixed,
+    })
+  }
+
+  // 收集数据
+  if (
+    columns.value.length > 0 &&
+    columns.value[columns.value.length - 1].dataIndex !== '__operation' &&
+    options.value.operationColumn
+  ) {
+    columns.value?.push({
+      title: options.value.operationColumnText || '操作',
+      dataIndex: '__operation',
+      slotName: '__operation',
+      align: 'center',
+      fixed: 'right',
+      width: options.value.operationColumnWidth ?? 150,
+    })
+  }
+  columns.value.forEach((element) => {
+    if (element.sortable) {
+      element.checked = true
+    }
+  })
+  if (isSort.value) {
+    const fromSearch = cloneDeep(searchForm.value)
+    if (!isUndefined(fromSearch.orderBy)) {
+      delete fromSearch.orderBy
+      delete fromSearch.orderType
+    }
+    requestParams.value = Object.assign(requestParams.value, fromSearch)
+  } else {
+    requestParams.value = Object.assign(requestParams.value, searchForm.value)
+  }
+  if (options.value.singleLine) {
+    singleLine.value = options.value.singleLine
+  } else {
+    singleLine.value = Object.getOwnPropertyNames(props.searchForm).length > 3 ? false : true
+  }
+  currentApi.value = options.value.api
+}
+
+const refresh = async () => {
+  await requestData()
+  //tableRef.value?.selectAll(false)
+}
+
+const requestData = async () => {
+  loading.value = true
+  init()
+  if (isFunction(currentApi.value)) {
+    const response = await currentApi.value(requestParams.value)
+    if (response.data && response.data.data) {
+      tableData.total = response.data.total
+      tableData.data = response.data.data
+    } else {
+      tableData.total = 0
+      tableData.data = response.data
+    }
+  } else {
+    console.error(`sa-table error:crud.api is not Function.`)
+  }
+  loading.value = false
+}
+
+// 添加操作
+const addAction = () => {
+  if (options.value.add.func && isFunction(options.value.add.func)) {
+    options.value.add.func()
+  } else {
+    console.error(`sa-table error:crud.add.func is not Function.`)
+  }
+}
+
+// 编辑操作
+const editAction = (record) => {
+  if (options.value.edit.func && isFunction(options.value.edit.func)) {
+    options.value.edit.func(record)
+  } else {
+    console.error(`sa-table error:crud.edit.func is not Function.`)
+  }
+}
+
+// 查看操作
+const viewAction = (record) => {
+  if (options.value.view.func && isFunction(options.value.view.func)) {
+    options.value.view.func(record)
+  } else {
+    console.error(`sa-table error:crud.view.func is not Function.`)
+  }
+}
+
+// 删除操作
+const deleteAction = async (record) => {
+  const params = { ids: [record[options.value.pk]] }
+  if (options.value.delete.func && isFunction(options.value.delete.func)) {
+    options.value.delete.func(params)
+  } else {
+    console.error(`sa-table error:crud.delete.func is not Function.`)
+  }
+}
+
+// 批量删除
+const deletesMultipleAction = async () => {
+  const params = { ids: selecteds.value }
+  if (selecteds.value && selecteds.value.length > 0) {
+    // 删除
+    if (options.value.delete.func && isFunction(options.value.delete.func)) {
+      options.value.delete.func(params)
+      tableRef.value?.selectAll(false)
+    } else {
+      console.error(`sa-table error:crud.delete.func is not Function.`)
+    }
+  } else {
+    Message.error('至少选择一条数据')
+  }
+}
+
+// 导入
+const importAction = () => crudImportRef.value.open()
+
+// 导出
+const exportAction = () => {
+  Message.info('请求服务器下载文件中...')
+  const data = requestParams.value
+  const download = (url) => request({ url, data, method: 'post', timeout: 60 * 1000, responseType: 'blob' })
+  isExport.value = true
+  download(options.value.export.url)
+    .then((res) => {
+      if (res && res.status == 200) {
+        tool.download(res)
+        Message.success('请求成功,文件开始下载')
+      } else {
+        Message.error('请前往服务端安装Excel导出库')
+      }
+    })
+    .catch(() => {
+      Message.error('请求服务器错误,下载失败')
+    })
+    .finally(() => {
+      isExport.value = false
+    })
+}
+
+const imageSee = async (row, record, dataIndex) => {
+  imgUrl.value = record[dataIndex]
+  imgVisible.value = true
+}
+
+const imageView = (url) => {
+  if (typeof url === 'string' && url !== null) {
+    return url
+  } else {
+    if (url !== null) {
+      return url[0]
+    } else {
+      return import.meta.env.VITE_APP_BASE + 'not-image.png'
+    }
+  }
+}
+
+const resizeHandler = () => {
+  headerHeight.value = crudHeaderRef.value.offsetHeight
+  settingFixedPage()
+}
+
+const settingFixedPage = () => {
+  const workAreaHeight = options.value.height ? options.value.height : document.querySelector('.work-area').offsetHeight
+  let tableHeight = workAreaHeight - headerHeight.value - 160 + (!showSearch.value ? 15 : 0)
+  crudContentRef.value.style.height = tableHeight + 'px'
+}
+
+onMounted(async () => {
+  showSearch.value = options.value.showSearch ?? true
+  if (options.value.pageLayout === 'fixed') {
+    await nextTick(() => {
+      window.addEventListener('resize', resizeHandler, false)
+      headerHeight.value = crudHeaderRef.value.offsetHeight
+      settingFixedPage()
+    })
+  }
+})
+
+onUnmounted(() => {
+  if (options.value.pageLayout === 'fixed') {
+    window.removeEventListener('resize', resizeHandler, false)
+  }
+})
+
+defineExpose({
+  requestData,
+  refresh,
+  setSelecteds,
+  clearSelected,
+  tableRef,
+  getTableData,
+  getTableTotal,
+})
+</script>
+
+<style lang="less" scoped>
+.setting {
+  display: flex;
+  align-items: center;
+  width: 150px;
+  .title {
+    margin-left: 12px;
+    cursor: pointer;
+  }
+}
+</style>

+ 120 - 0
src/components/sa-treeSlider/index.vue

@@ -0,0 +1,120 @@
+<template>
+  <div class="flex flex-col w-full" :class="props.border ? 'slider-border p-2' : ''">
+    <a-input-group class="mb-2 w-full" size="mini">
+      <a-input :placeholder="props?.searchPlaceholder" allow-clear @input="changeKeyword" @clear="resetData" />
+      <a-button
+        @click="
+          () => {
+            isExpand ? saTree.expandAll(false) : saTree.expandAll(true)
+            isExpand = !isExpand
+          }
+        "
+        >{{ isExpand ? '折叠' : '展开' }}</a-button
+      >
+      <slot name="treeAfterButtons"></slot>
+    </a-input-group>
+    <a-tree
+      blockNode
+      ref="saTree"
+      :data="treeData"
+      class="h-full w-full"
+      @select="handlerSelect"
+      :field-names="props.fieldNames"
+      v-model:selected-keys="modelValue"
+      v-bind="$attrs">
+      <template #icon v-if="props.icon"><component :is="props.icon" /></template>
+      <template v-for="(_, name) in $slots" v-slot:[name]="data">
+        <slot :name="name" v-bind="data"></slot>
+      </template>
+    </a-tree>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch, computed, onMounted } from 'vue'
+
+const treeData = ref([])
+const saTree = ref()
+const isExpand = ref(false)
+
+const emit = defineEmits(['update:modelValue', 'click'])
+
+const props = defineProps({
+  modelValue: { type: Array },
+  data: { type: Array },
+  border: { type: Boolean, default: true },
+  searchPlaceholder: { type: String },
+  fieldNames: {
+    type: Object,
+    default: () => {
+      return { title: 'label', key: 'value' }
+    },
+  },
+  icon: { type: String, default: undefined },
+})
+
+const modelValue = computed({
+  get() {
+    return props.modelValue
+  },
+  set(newVal) {
+    emit('update:modelValue', newVal)
+  },
+})
+
+watch(
+  () => props.data,
+  (val) => {
+    treeData.value = val
+  },
+  { immediate: true, deep: true }
+)
+
+const handlerSelect = (item, data) => {
+  modelValue.value = [item]
+  emit('click', ...[item, data])
+}
+
+const resetData = () => (treeData.value = props.data)
+
+const changeKeyword = (keyword) => {
+  if (!keyword || keyword === '') {
+    treeData.value = Object.assign(props.data, [])
+    return false
+  }
+  treeData.value = searchNode(keyword)
+}
+
+const searchNode = (keyword) => {
+  const loop = (data) => {
+    let tree = []
+    data.map((item) => {
+      if (item[props.fieldNames['title']].indexOf(keyword) !== -1) {
+        tree.push(item)
+      } else if (item.children && item.children.length > 0) {
+        const temp = loop(item.children)
+        tree.push(...temp)
+      }
+      return tree
+    })
+
+    return tree
+  }
+  return loop(treeData.value)
+}
+
+defineExpose({ saTree })
+</script>
+
+<style scoped lang="less">
+:deep(.arco-tree-node:hover) {
+  background-color: var(--color-fill-2);
+  border-radius: 3px;
+}
+:deep(.arco-tree-node-switcher) {
+  margin-left: 2px;
+}
+.slider-border {
+  border: 1px solid #ebebeb;
+}
+</style>

+ 206 - 0
src/components/sa-upload-file/index.vue

@@ -0,0 +1,206 @@
+<template>
+  <div>
+    <div class="upload-file w-full">
+      <a-upload
+        :custom-request="uploadFileHandler"
+        :show-file-list="false"
+        :multiple="props.multiple"
+        :accept="props.accept"
+        :disabled="isDisabled"
+        :tip="props.tip"
+        :draggable="props.draggable">
+        <template #upload-button v-if="props.draggable">
+          <slot name="customer">
+            <div style="background-color: var(--color-fill-2); border: 1px dashed var(--color-fill-4)" class="rounded text-center p-7 w-full">
+              <div>
+                <icon-upload class="text-5xl text-gray-400" />
+                <div class="text-red-600 font-bold">
+                  {{ props.title }}
+                </div>
+                将文件拖到此处,或<span style="color: #3370ff">点击上传</span>
+              </div>
+            </div>
+          </slot>
+        </template>
+      </a-upload>
+    </div>
+    <!-- 单文件 -->
+    <div class="file-list mt-2" v-if="!props.multiple && currentItem?.url && props.showList">
+      <a-tooltip content="点击文件名预览/下载" position="tr">
+        <a :href="currentItem.url" v-if="currentItem?.url" class="file-name" target="_blank">{{ currentItem.name }}</a>
+      </a-tooltip>
+
+      <a-button type="text" size="small" @click="removeSignFile()">
+        <template #icon>
+          <icon-delete />
+        </template>
+      </a-button>
+    </div>
+
+    <!-- 多文件 -->
+    <div v-if="props.showList" class="file-list mt-2" v-for="(file, idx) in showFileList" :key="idx">
+      <a-tooltip content="点击文件名预览/下载" position="tr">
+        <a :href="file.url" v-if="file?.url" class="file-name" target="_blank">{{ file.name }}</a>
+      </a-tooltip>
+
+      <a-button type="text" size="small" @click="removeFile(idx)">
+        <template #icon>
+          <icon-delete />
+        </template>
+      </a-button>
+    </div>
+  </div>
+</template>
+<script setup>
+import { ref, watch, computed } from 'vue'
+import { isArray } from 'lodash'
+import file2md5 from 'file2md5'
+import commonApi from '@/api/common'
+import { Message } from '@arco-design/web-vue'
+
+const props = defineProps({
+  modelValue: {
+    type: [String, Number, Array],
+    default: () => {},
+  },
+  showList: { type: Boolean, default: true },
+  draggable: { type: Boolean, default: false },
+  multiple: { type: Boolean, default: false },
+  disabled: { type: Boolean, default: false },
+  title: { type: String, default: '本地上传' },
+  icon: { type: String, default: 'icon-plus' },
+  size: { type: Number, default: 4 * 1024 * 1024 },
+  limit: { type: Number, default: 0 },
+  mode: { type: String, default: 'system' },
+  tip: { type: String, default: undefined },
+  accept: { type: String, default: '*' },
+})
+const emit = defineEmits(['update:modelValue'])
+const showFileList = ref([])
+const signFile = ref()
+const currentItem = ref({})
+
+const isDisabled = computed(() => {
+  if (props.disabled) {
+    return true
+  } else {
+    if (!props.multiple) {
+      if (currentItem.value && currentItem.value.url) {
+        return true
+      }
+    }
+    return false
+  }
+})
+
+const uploadFileHandler = async (options) => {
+  if (!options.fileItem) return
+  if (!props.multiple) {
+    currentItem.value = options.fileItem
+  }
+  let isCheck = true
+  const file = options.fileItem.file
+  if (file.size > props.size) {
+    Message.warning(file.name + '超出文件大小限制')
+    currentItem.value = {}
+    isCheck = false
+  }
+  if (props.multiple && props.limit > 0) {
+    if (showFileList.value.length >= props.limit) {
+      Message.warning('最多上传' + props.limit + '个文件')
+      currentItem.value = {}
+      isCheck = false
+    }
+  }
+
+  if (isCheck) {
+    const hash = await file2md5(file)
+    const dataForm = new FormData()
+    dataForm.append('file', file)
+    dataForm.append('hash', hash)
+    if (props.mode === 'local') {
+      dataForm.append('mode', 'local')
+    }
+    const resp = await commonApi.uploadFile(dataForm)
+    const result = resp.data
+    if (result) {
+      if (!props.multiple) {
+        signFile.value = result['url']
+        emit('update:modelValue', signFile.value)
+      } else {
+        showFileList.value.push(result)
+        let files = []
+        files = showFileList.value.map((item) => {
+          return item['url']
+        })
+        emit('update:modelValue', files)
+      }
+    }
+  }
+}
+
+const removeSignFile = () => {
+  currentItem.value = {}
+  signFile.value = undefined
+  emit('update:modelValue', null)
+}
+
+const removeFile = (idx) => {
+  showFileList.value.splice(idx, 1)
+  let files = []
+  files = showFileList.value.map((item) => {
+    return item['url']
+  })
+  emit('update:modelValue', files)
+}
+
+const initData = async () => {
+  if (props.multiple) {
+    if (isArray(props.modelValue) && props.modelValue.length > 0) {
+      showFileList.value = props.modelValue.map((url) => {
+        return { url, name: url.substring(url.lastIndexOf('/') + 1) }
+      })
+    } else {
+      showFileList.value = []
+    }
+  } else if (props.modelValue) {
+    signFile.value = props.modelValue
+    currentItem.value.url = props.modelValue
+    currentItem.value.name = props.modelValue.substring(props.modelValue.lastIndexOf('/') + 1)
+  } else {
+    removeSignFile()
+  }
+}
+
+watch(
+  () => props.modelValue,
+  (val) => {
+    initData()
+  },
+  {
+    deep: true,
+    immediate: true,
+  }
+)
+</script>
+
+<style lang="less" scoped>
+.file-list {
+  background-color: var(--color-primary-light-1);
+  border-radius: 4px;
+  height: 36px;
+  padding: 0 5px;
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+
+  .file-name {
+    max-width: 90%;
+    margin: 0 5px;
+    overflow: hidden;
+    color: #165dff;
+  }
+}
+</style>

+ 221 - 0
src/components/sa-upload-image/index.vue

@@ -0,0 +1,221 @@
+<template>
+  <div class="upload-image flex" :class="props.rounded ? 'rounded-full' : ''">
+    <!-- 单图 -->
+    <a-space wrap>
+      <div :class="'image-list ' + (props.rounded ? 'rounded-full' : '')" v-if="!props.multiple && currentItem?.url">
+        <a-button class="delete" @click="removeSignImage()">
+          <template #icon>
+            <icon-delete />
+          </template>
+        </a-button>
+        <a-image width="130" height="130" :class="props.rounded ? 'rounded-full' : ''" :src="currentItem.url" />
+      </div>
+      <!-- 多图显示 -->
+      <template v-else-if="props.multiple">
+        <div :class="'image-list ' + (props.rounded ? 'rounded-full' : '')" v-for="(image, idx) in showImgList" :key="idx">
+          <a-button class="delete" @click="removeImage(idx)">
+            <template #icon>
+              <icon-delete />
+            </template>
+          </a-button>
+          <a-image width="130" height="130" :class="props.rounded ? 'rounded-full' : ''" :src="image.url" />
+        </div>
+      </template>
+
+      <a-upload
+        :custom-request="uploadImageHandler"
+        :show-file-list="false"
+        :accept="props.accept ?? '.jpg,.jpeg,.gif,.png,.svg,.bpm'"
+        :disabled="props.disabled"
+        :tip="props.tip">
+        <template #upload-button>
+          <slot name="customer">
+            <div :class="'upload-skin ' + (props.rounded ? 'rounded-full' : 'rounded-sm')" v-if="!props.modelValue || props.multiple">
+              <div class="icon text-3xl">
+                <component :is="props.icon" />
+              </div>
+              <div class="title">
+                {{ props.title }}
+              </div>
+            </div>
+          </slot>
+        </template>
+      </a-upload>
+    </a-space>
+  </div>
+</template>
+<script setup>
+import { ref, watch } from 'vue'
+import { isArray } from 'lodash'
+import file2md5 from 'file2md5'
+import commonApi from '@/api/common'
+import { Message } from '@arco-design/web-vue'
+
+const props = defineProps({
+  modelValue: {
+    type: [String, Number, Array],
+    default: () => {},
+  },
+  rounded: { type: Boolean, default: false },
+  multiple: { type: Boolean, default: false },
+  disabled: { type: Boolean, default: false },
+  title: { type: String, default: '本地上传' },
+  icon: { type: String, default: 'icon-plus' },
+  size: { type: Number, default: 4 * 1024 * 1024 },
+  limit: { type: Number, default: 0 },
+  mode: { type: String, default: 'system' },
+  tip: { type: String, default: undefined },
+  accept: { type: String, default: '.jpg,.jpeg,.gif,.png,.svg,.bpm' },
+})
+const emit = defineEmits(['update:modelValue'])
+
+const showImgList = ref([])
+const signImage = ref()
+const currentItem = ref({})
+
+const uploadImageHandler = async (options) => {
+  if (!options.fileItem) return
+  if (!props.multiple) {
+    currentItem.value = options.fileItem
+  }
+  let isCheck = true
+  const file = options.fileItem.file
+  if (file.size > props.size) {
+    Message.warning(file.name + '超出文件大小限制')
+    currentItem.value = {}
+    isCheck = false
+  }
+  if (props.multiple && props.limit > 0) {
+    if (showImgList.value.length >= props.limit) {
+      Message.warning('最多上传' + props.limit + '张图片')
+      currentItem.value = {}
+      isCheck = false
+    }
+  }
+
+  if (isCheck) {
+    const hash = await file2md5(file)
+    const dataForm = new FormData()
+    dataForm.append('image', file)
+    dataForm.append('isChunk', false)
+    dataForm.append('hash', hash)
+    if (props.mode === 'local') {
+      dataForm.append('mode', 'local')
+    }
+    const resp = await commonApi.uploadImage(dataForm)
+    const result = resp.data
+    if (result) {
+      if (!props.multiple) {
+        signImage.value = result['url']
+        emit('update:modelValue', signImage.value)
+      } else {
+        showImgList.value.push(result)
+        let files = []
+        files = showImgList.value.map((item) => {
+          return item['url']
+        })
+        emit('update:modelValue', files)
+      }
+    }
+  }
+}
+
+const removeSignImage = () => {
+  currentItem.value = {}
+  signImage.value = undefined
+  emit('update:modelValue', null)
+}
+
+const removeImage = (idx) => {
+  showImgList.value.splice(idx, 1)
+  let files = []
+  files = showImgList.value.map((item) => {
+    return item['url']
+  })
+  emit('update:modelValue', files)
+}
+
+const initData = async () => {
+  if (props.multiple) {
+    if (isArray(props.modelValue) && props.modelValue.length > 0) {
+      showImgList.value = props.modelValue.map((url) => {
+        return { url }
+      })
+    } else {
+      showImgList.value = []
+    }
+  } else if (props.modelValue) {
+    signImage.value = props.modelValue
+    currentItem.value.url = props.modelValue
+    currentItem.value.percent = 100
+    currentItem.value.status = 'complete'
+  } else {
+    removeSignImage()
+  }
+}
+
+watch(
+  () => props.modelValue,
+  (val) => {
+    initData()
+  },
+  {
+    deep: true,
+    immediate: true,
+  }
+)
+</script>
+
+<style lang="less" scoped>
+.upload-skin {
+  background-color: var(--color-fill-2);
+  border: 1px dashed var(--color-fill-4);
+  width: 130px;
+  height: 130px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+
+  .icon,
+  .title {
+    color: var(--color-text-3);
+  }
+}
+
+.image-list {
+  cursor: pointer;
+  position: relative;
+  background-color: var(--color-fill-2);
+  width: 130px;
+  height: 130px;
+
+  .delete {
+    position: absolute;
+    z-index: 99;
+    right: 3px;
+    top: 3px;
+    display: none;
+  }
+
+  .progress {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translateX(-50%) translateY(-50%);
+  }
+}
+
+.image-list:hover {
+  .delete {
+    display: block;
+  }
+}
+
+.upload-skin:hover {
+  border: 1px dashed rgb(var(--primary-6));
+}
+// .arco-upload-hide {
+//   display: block !important;
+// }
+</style>

+ 159 - 0
src/components/sa-user/index.vue

@@ -0,0 +1,159 @@
+<template>
+  <div class="ma-content-block">
+    <a-space class="flex">
+      <a-button type="primary" @click="open">
+        <template #icon><icon-select-all /></template>{{ props.text }}
+      </a-button>
+      <a-tag size="large" color="blue" v-if="props.isEcho"
+        >已选择 {{ isArray(selecteds) ? selecteds.length : 0 }} 位</a-tag
+      >
+      <a-input-tag
+        v-model="userList"
+        v-if="props.isEcho"
+        :style="{ width: '320px' }"
+        :placeholder="'请点击前面按钮' + props.text"
+        :max-tag-count="3"
+        disabled />
+    </a-space>
+
+    <a-modal v-model:visible="visible" width="100%" draggable :on-before-ok="close" unmountOnClose>
+      <template #title>{{ props.text }}</template>
+
+      <sa-table
+        ref="crudRef"
+        :options="options"
+        :columns="columns"
+        :searchForm="searchForm"
+        v-model:selected-keys="selecteds"
+        @selection-change="selectHandler">
+        <!-- 搜索区 tableSearch -->
+        <template #tableSearch>
+          <a-col :sm="8" :xs="24">
+            <a-form-item field="username" label="账户">
+              <a-input v-model="searchForm.username" placeholder="请输入账户" />
+            </a-form-item>
+          </a-col>
+          <a-col :sm="8" :xs="24">
+            <a-form-item field="phone" label="手机">
+              <a-input v-model="searchForm.phone" placeholder="请输入手机" />
+            </a-form-item>
+          </a-col>
+          <a-col :sm="8" :xs="24">
+            <a-form-item field="dept_id" label="部门">
+              <a-tree-select
+                v-model="searchForm.dept_id"
+                :data="deptData"
+                :field-names="{ key: 'value', title: 'label' }"
+                allow-clear
+                placeholder="请选择所属部门">
+              </a-tree-select>
+            </a-form-item>
+          </a-col>
+        </template>
+      </sa-table>
+    </a-modal>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref, watch, nextTick } from 'vue'
+import commonApi from '@/api/common'
+import { Message } from '@arco-design/web-vue'
+import { isArray, isEmpty } from 'lodash'
+
+const props = defineProps({
+  modelValue: { type: Array },
+  isEcho: { type: Boolean, default: false },
+  multiple: { type: Boolean, default: true },
+  onlyId: { type: Boolean, default: true },
+  text: { type: String, default: '选择用户' },
+})
+
+const emit = defineEmits(['update:modelValue', 'success'])
+
+const visible = ref(false)
+const crudRef = ref()
+const selecteds = ref([])
+const userList = ref([])
+const deptData = ref([])
+
+const open = async () => {
+  visible.value = true
+  initPage()
+  await nextTick()
+  crudRef.value?.refresh()
+
+  setTimeout(() => {
+    selecteds.value = props.modelValue
+  }, 500)
+}
+
+const initPage = async () => {
+  const deptResp = await commonApi.commonGet('/core/dept/index?tree=true')
+  deptData.value = deptResp.data
+}
+
+onMounted(() => {
+  if (props.isEcho && props.onlyId) selecteds.value = props.modelValue
+})
+
+watch(
+  () => props.modelValue,
+  (val) => {
+    if (props.isEcho && props.onlyId) selecteds.value = val
+    if (val.length == 0) userList.value = []
+  }
+)
+
+const selectHandler = (rows) => {
+  selecteds.value = rows
+}
+
+const close = async (done) => {
+  if (isArray(selecteds.value) && selecteds.value.length > 0) {
+    const response = await commonApi.getUserInfoByIds({ ids: selecteds.value })
+    if (!isEmpty(response) && isArray(response.data)) {
+      userList.value = response.data.map((item) => {
+        return `${item.username}(${item.id})`
+      })
+      if (props.onlyId) {
+        emit('update:modelValue', selecteds.value)
+      } else {
+        emit('update:modelValue', response.data)
+      }
+      emit('success', true)
+      Message.success('选择成功')
+    }
+  } else {
+    emit('update:modelValue', [])
+    userList.value = []
+  }
+  done(true)
+}
+
+const searchForm = ref({
+  username: '',
+  phone: '',
+  dept_id: '',
+})
+
+const options = ref({
+  api: commonApi.getUserList,
+  pageSimple: true,
+  operationColumn: false,
+  rowSelection: props.multiple ? { type: 'checkbox', showCheckedAll: true } : { type: 'radio' },
+})
+
+const columns = ref([
+  { title: '账户', dataIndex: 'username', width: 120 },
+  { title: '昵称', dataIndex: 'nickname', width: 120 },
+  { title: '手机', dataIndex: 'phone', width: 120 },
+  { title: '邮箱', dataIndex: 'email', width: 180 },
+])
+</script>
+
+<style scoped>
+:deep(.arco-tabs-nav-type-capsule .arco-tabs-nav-tab) {
+  justify-content: flex-start;
+}
+</style>

+ 18 - 0
src/config/skins.js

@@ -0,0 +1,18 @@
+export default [
+  {
+    name: 'mine',
+    thumb: 'skins-thumb/mine/thumb.jpg',
+  },
+  {
+    name: 'classics',
+    thumb: 'skins-thumb/classics/thumb.jpg',
+  },
+  {
+    name: 'businessGray',
+    thumb: 'skins-thumb/businessGray/thumb.jpg',
+  },
+  {
+    name: 'city',
+    thumb: 'skins-thumb/city/thumb.jpg',
+  }
+]

+ 8 - 0
src/directives/auth/auth.js

@@ -0,0 +1,8 @@
+import { useUserStore } from '@/store'
+
+const auth = name => {
+  const userStore = useUserStore()
+  return (userStore.codes && userStore.codes.includes(name)) || (userStore.codes && userStore.codes.includes('*'))
+}
+
+export default auth

+ 29 - 0
src/directives/auth/index.js

@@ -0,0 +1,29 @@
+import auth from './auth'
+
+const checkAuth = (el, binding) => {
+  const { value } = binding
+
+  if (Array.isArray(value)) {
+    if (value.length > 0) {
+      let isHas = false
+      value.map(item => {
+        isHas = auth(item)
+      })
+
+      if (!isHas && el.parentNode) {
+        el.parentNode.removeChild(el)
+      }
+    }
+  } else {
+    throw new Error(`need permission! Like v-auth="['admin','user']"`)
+  }
+}
+
+export default {
+  mounted(el, binding) {
+    checkAuth(el, binding)
+  },
+  updated(el, binding) {
+    checkAuth(el, binding)
+  },
+};

+ 27 - 0
src/directives/copy/index.js

@@ -0,0 +1,27 @@
+import useClipboard from 'vue-clipboard3'
+import { Message } from '@arco-design/web-vue'
+
+const copy = (el, binding) => {
+  const { value } = binding
+  el.addEventListener('click', async () => {
+    if (value && value !== '') {
+      try {
+        await useClipboard().toClipboard(value)
+        Message.success('已成功复制到剪切板')
+      } catch(e) {
+        Message.error('复制失败')
+      }
+    } else {
+      throw new Error(`need for copy content! Like v-copy="Hello World"`)
+    }
+  })
+}
+
+export default {
+  mounted(el, binding) {
+    copy(el, binding)
+  },
+  updated(el, binding) {
+    copy(el, binding)
+  },
+};

+ 12 - 0
src/directives/index.js

@@ -0,0 +1,12 @@
+import auth from './auth/index'
+import role from './role/index'
+import copy from './copy/index'
+
+
+export default {
+  install (Vue) {
+    Vue.directive('auth', auth)
+    Vue.directive('role', role)
+    Vue.directive('copy', copy)
+  }
+}

+ 31 - 0
src/directives/role/index.js

@@ -0,0 +1,31 @@
+import role from './role'
+
+const checkRole = (el, binding) => {
+  const { value } = binding
+
+  if (Array.isArray(value)) {
+    if (value.length > 0) {
+      let isHas = false
+      value.map(item => {
+        if(!isHas) {
+          isHas = role(item)
+        }
+      })
+
+      if (!isHas && el.parentNode) {
+        el.parentNode.remove()
+      }
+    }
+  } else {
+    throw new Error(`need role! Like v-role="['seo', 'cfo']"`)
+  }
+}
+
+export default {
+  mounted(el, binding) {
+    checkRole(el, binding)
+  },
+  updated(el, binding) {
+    checkRole(el, binding)
+  },
+};

+ 8 - 0
src/directives/role/role.js

@@ -0,0 +1,8 @@
+import { useUserStore } from '@/store'
+
+const role = name => {
+  const userStore = useUserStore()
+  return (userStore.roles && userStore.roles.includes(name)) || (userStore.roles && userStore.roles.includes('superAdmin'))
+}
+
+export default role

+ 3 - 0
src/i18n/en/crud.js

@@ -0,0 +1,3 @@
+export default {
+  
+}

+ 8 - 0
src/i18n/en/maResource.js

@@ -0,0 +1,8 @@
+export default {
+  loadingText: 'Loading...',
+  searchFileNotice: 'Search file by name',
+  searchResource: 'Search resource type',
+  saveNetworkImage: 'Save network image',
+  networkImageNotice: 'Please paste the web picture address',
+  ok: 'OK',
+}

+ 53 - 0
src/i18n/en/menus.js

@@ -0,0 +1,53 @@
+export default {
+  // 特殊页
+  'openForm': 'CRUD',
+
+  // 首页菜单
+  'home': 'Home',
+  'dashboard': 'Dashboard',
+  'userCenter': 'User Center',
+  'message': 'Message Center',
+  'setting:config': 'System Setting',
+  'demo': 'Component Demo',
+
+  // 权限
+  'permission': 'Permission',
+  'system:user': 'User Manage',
+  'system:role': 'Role Manage',
+  'system:dept': 'Department Manage',
+  'system:menu': 'Menu Manage',
+  'system:post': 'Post Manage',
+
+  'dataCenter': 'Data Center',
+  'system:dict': 'Dictionary',
+  'system:attachment': 'Attached',
+  'system:dataMaintain': 'Table Maintenance',
+  'system:notice': 'Notice',
+  'apps': 'App Center',
+  'system:appGroup': 'App Group',
+  'system:app': 'App Manage',
+  'apis': 'Api Center',
+  'system:apiGroup': 'Api Group',
+  'system:api': 'Api Manage',
+
+  // 监控
+  'monitor': 'Monitor',
+  'system:monitor:server': 'Server Monitor',
+  'system:onlineUser': 'Online User',
+  'system:cache': 'Cache Monitor',
+  'system:monitor:rely': 'Reliance Monitor',
+  'logs': 'Logs Monitor',
+  'system:queueLog': 'Queue Logs',
+  'system:loginLog': 'Login Logs',
+  'system:operLog': 'Operation Logs',
+  'system:apiLog': 'Apis Logs',
+
+  // 工具
+  'devTools': 'Tools',
+  'setting:module': 'Module Manage',
+  'setting:code': 'Code Generator',
+  'setting:code:update': 'Edit the build information',
+  'setting:crontab': 'Crontab',
+  'setting:table': 'Table Designer',
+  'systemInterface': 'System Apis',
+  }

+ 14 - 0
src/i18n/en/skin.js

@@ -0,0 +1,14 @@
+export default {
+  mine: 'Mine',
+  classics: 'classics',
+  businessGray: 'Business gray',
+  city: 'City',
+
+  mineDesc: 'Predominantly pure white, Mine defaults to skin',
+  classicsDesc: 'Classic dark sidebar skin',
+  businessGrayDesc: 'Gray versatility and atmosphere, creating business and stability',
+  cityDesc: 'May there be a warmth in every angle of the city',
+
+  activated: 'Activated',
+  use: 'Use'
+}

+ 92 - 0
src/i18n/en/sys.js

@@ -0,0 +1,92 @@
+export default {
+  pageSetting: 'Page Setting',
+  chinese: '简体中文',
+  english: 'English',
+  search: 'Search',
+  store: 'App Store',
+  fullScreen: 'Full Screen',
+  closeFullScreen: 'Close Full Screen',
+  changeSkin: 'Change Skin',
+  skin: 'Skin',
+  layouts: 'Layout',
+  language: 'Language',
+  dark: 'Dark Mode',
+  tag: 'Open Tags',
+  water: 'Watermark',
+  waterContent: 'Watermark content',
+  menuFold: 'Menu Fold',
+  menuWidth: 'Mene Width',
+  skinHelp: 'Set up background skins',
+  layoutsHelp: 'Set the background display',
+  languageHelp: 'Set the page language and the request background language',
+  darkHelp: 'Sets the page display mode',
+  tagHelp: 'Whether to enable multi-tab mode',
+  waterHelp: 'Whether to display the watermark',
+  menuFoldHelp: 'Whether the left menu of the system is collapsed',
+  menuWidthHelp: 'Sets the display width of the left menu',
+  saveToBackend: 'Save to backend',
+  backendSettingTitle: 'Backend setting',
+  systemPrimaryColor: 'System Primary Color',
+  personalizedConfig: 'Personalized configuration',
+  layout: {
+    classic: 'Classic',
+    columns: 'Columns',
+    banner: 'Banner',
+    mixed: 'Mixed',
+  },
+  userCenter: 'User Center',
+  clearCache: 'Clear Cache',
+  logout: 'Logout System',
+  logoutAlert: 'Exit prompt',
+  logoutMessage: 'Are you sure you want to sign out?',
+  operationMessage: {
+    message: 'Message',
+    notification: 'Notification',
+    todo: 'Todo',
+  },
+  goHome: 'Go Home',
+  notFoundPage: 'Exit tip Ah oh, the page visited was hijacked by the Martians...',
+  login: {
+    slogan: 'High-quality middle and back office management system out of the box',
+    title: 'Login System',
+    username: 'Username',
+    usernameNotice: 'Please enter the username',
+    password: 'Passoword',
+    passwordNotice: 'Please enter the password',
+    verifyCode: 'Please enter the verification code',
+    verifyCodeNotice: 'Please enter the correct verification code',
+    loginBtn: 'Login in',
+    otherLoginType: 'Other ways to sign in'
+  },
+  verifyCode: {
+    switch: 'Click Toggle verification code',
+    error: 'The verification code is incorrect',
+    notice: 'Please enter the verification code'
+  },
+  i18n: 'open multi-language',
+  i18nHelp: 'Whether to enable the multi-language feature',
+  ws: 'open websocket',
+  wsHelp: 'Whether to enable the websocket feature',
+  round: 'opend round',
+  roundHelp: 'Whether to enable the round feature',
+  animation: 'Animation',
+  animationHelp: 'Page transition animation effect',
+  animate: {
+    fade: 'The page fades out',
+    sliderLeft: 'The page fades to the left',
+    sliderRight:'The page fades to the right',
+    sliderDown:'The page fades to the down',
+    sliderUp:'The page fades to the up',
+  },
+  tags: {
+    refresh: 'Refresh',
+    fullscreen: 'Full screen',
+    closeRightTag: 'Close right tag',
+    closeLeftTag: 'Close left tag',
+    closeTag: 'Close current tag',
+    closeOtherTag: 'Close other tag',
+  },
+  noticeTitle: 'System Prompted',
+  save: 'Save',
+  cancel: 'Cancel',
+}

+ 8 - 0
src/i18n/en/upload.js

@@ -0,0 +1,8 @@
+export default {
+  fileHashFail: 'Get file hash failed, please try again!',
+  sizeLimit: 'The file size exceeds the upload limit',
+  uploadFailed: 'File upload failed',
+  buttonText: 'Local upload',
+  clickUpload: 'Click upload',
+  uploadDesc: 'Drag the file here, or ',
+}

+ 0 - 0
src/i18n/en/user.js


+ 50 - 0
src/i18n/index.js

@@ -0,0 +1,50 @@
+import { createI18n } from 'vue-i18n'
+import tool from '@/utils/tool'
+
+const setting = tool.local.get('setting')
+
+const getLanguage = () => {
+  const loadFile = () => {
+    if (setting.language === 'zh_CN') {
+      return import.meta.glob('./zh_CN/**/*.js', { eager:true })
+    } else if (setting.language === 'en') {
+      return import.meta.glob('./en/**/*.js', { eager:true })
+    }
+  }
+
+  const generateLanguage = (fileNames, fileContent, generateLanguages = {}) => {
+    const fileName = fileNames.shift()
+    if (fileNames.length > 0) {
+      if (typeof generateLanguages[fileName] == 'undefined') {
+        generateLanguages[fileName] = {}
+      }
+      generateLanguages[fileName] = generateLanguage(fileNames, fileContent, generateLanguages[fileName])
+    }else{
+      generateLanguages[fileName] = fileContent
+    }
+    return generateLanguages
+  }
+
+  const files = loadFile()
+  let messages = { [setting.language]: {} }
+  for (let path in files) {
+    const names = path.match(/([A-Za-z0-9_]+)/g)
+    //去除语言文件夹和文件后缀名
+    names.shift()
+    names.pop()
+    if (files[path].default) {
+      messages[setting.language] = generateLanguage(names, files[path].default, messages[setting.language])
+    }
+  }
+  return messages
+}
+
+const i18n = createI18n({
+  locale: setting.language,
+  legacy: false,
+  globalInjection: true,
+  fallbackLocale: 'zh_CN',
+  messages: getLanguage()
+})
+
+export default i18n

+ 3 - 0
src/i18n/zh_CN/crud.js

@@ -0,0 +1,3 @@
+export default {
+  
+}

+ 8 - 0
src/i18n/zh_CN/maResource.js

@@ -0,0 +1,8 @@
+export default {
+  loadingText: '数据加载中...',
+  searchFileNotice: '文件名搜索',
+  searchResource: '搜索资源类型',
+  saveNetworkImage: '保存网络图片',
+  networkImageNotice: '请粘贴网络图片地址',
+  ok: '确定'
+}

+ 53 - 0
src/i18n/zh_CN/menus.js

@@ -0,0 +1,53 @@
+export default {
+  // 特殊页
+  'openForm': '公共表单',
+
+  // 首页菜单
+  'home': '首页',
+  'dashboard': '仪表盘',
+  'userCenter': '个人中心',
+  'message': '消息中心',
+  'setting:config': '系统配置',
+  'demo': '组件演示',
+
+  // 权限
+  'permission': '权限',
+  'system:user': '用户管理',
+  'system:role': '角色管理',
+  'system:dept': '部门管理',
+  'system:menu': '菜单管理',
+  'system:post': '岗位管理',
+
+  'dataCenter': '数据',
+  'system:dict': '数据字典',
+  'system:attachment': '附件管理',
+  'system:dataMaintain': '数据表维护',
+  'system:notice': '系统公告',
+  'apps': '应用中心',
+  'system:appGroup': '应用分组',
+  'system:app': '应用管理',
+  'apis': '应用接口',
+  'system:apiGroup': '接口分组',
+  'system:api': '接口管理',
+
+  // 监控
+  'monitor': '监控',
+  'system:monitor:server': '服务监控',
+  'system:onlineUser': '在线用户',
+  'system:cache': '缓存监控',
+  'system:monitor:rely': '依赖监控',
+  'logs': '日志监控',
+  'system:queueLog': '队列日志',
+  'system:loginLog': '登录日志',
+  'system:operLog': '操作日志',
+  'system:apiLog': '接口日志',
+
+  // 工具
+  'devTools': '工具',
+  'setting:module': '模块管理',
+  'setting:code': '代码生成器',
+  'setting:code:update': '编辑生成信息',
+  'setting:crontab': '定时任务',
+  'setting:table': '数据表设计器',
+  'systemInterface': '系统接口',
+}

+ 14 - 0
src/i18n/zh_CN/skin.js

@@ -0,0 +1,14 @@
+export default {
+    mine: 'Mine',
+    classics: '经典',
+    businessGray: '商务灰',
+    city: '城市',
+  
+    mineDesc: '以纯净的白色为主,Mine默认皮肤',
+    classicsDesc: '经典的深色侧边栏皮肤',
+    businessGrayDesc: '灰色的百搭与大气,营造商务与稳重',
+    cityDesc: '愿城市每一个角度,都有一份温馨',
+
+    activated: '已激活',
+    use: '使用'
+  }

+ 92 - 0
src/i18n/zh_CN/sys.js

@@ -0,0 +1,92 @@
+export default {
+  pageSetting: '页面设置',
+  chinese: '简体中文',
+  english: 'English',
+  search: '搜索',
+  store: '应用市场',
+  fullScreen: '全屏',
+  closeFullScreen: '关闭全屏',
+  changeSkin: '换肤',
+  skin: '当前皮肤',
+  layouts: '布局',
+  language: '语言',
+  dark: '黑夜模式',
+  tag: '多标签',
+  water: '水印',
+  waterContent: '水印内容',
+  menuFold: '菜单折叠',
+  menuWidth: '菜单宽度',
+  skinHelp: '设置后台皮肤',
+  layoutsHelp: '设置后台显示方式',
+  languageHelp: '设置页面语言和请求后台语言',
+  darkHelp: '设置页面显示模式',
+  tagHelp: '是否启用多标签方式',
+  waterHelp: '是否显示水印',
+  menuFoldHelp: '系统左侧菜单是否折叠起来',
+  menuWidthHelp: '设置左侧菜单的显示宽度',
+  saveToBackend: '保存到后台',
+  backendSettingTitle: '后台设置',
+  systemPrimaryColor: '系统主色调',
+  personalizedConfig: '个性化配置 ',
+  layout: {
+    classic: '经典',
+    columns: '分栏',
+    banner: '通栏',
+    mixed: '混合',
+  },
+  userCenter: '个人中心',
+  clearCache: '清除缓存',
+  logout: '退出系统',
+  logoutAlert: '退出提示',
+  logoutMessage: '确定要退出登录吗?',
+  operationMessage: {
+    message: '消息',
+    notification: '通知',
+    todo: '待办',
+  },
+  goHome: '回到首页',
+  notFoundPage: '啊哦,访问的页面被火星人劫走了...',
+  login: {
+    slogan: '开箱即用的高质量中后台管理系统',
+    title: '登录',
+    username: '账户',
+    usernameNotice: '请输入账户',
+    password: '密码',
+    passwordNotice: '请输入密码',
+    verifyCode: '请输入验证码',
+    verifyCodeNotice: '请输入正确的验证码',
+    loginBtn: '登录',
+    otherLoginType: '其他登录方式'
+  },
+  verifyCode: {
+    switch: '点击切换验证码',
+    error: '验证码错误',
+    notice: '请输入验证码'
+  },
+  i18n: '开启多语言',
+  i18nHelp: '是否开启多语言功能',
+  ws: '开启Ws',
+  wsHelp: '是否开启Websocket连接',
+  round: '圆角',
+  roundHelp: '是否开启圆角',
+  animation: '切换动画',
+  animationHelp: '工作区页面切换的进场和出场动画效果',
+  animate: {
+    fade: '页面渐隐渐出',
+    sliderLeft: '页面向左渐出',
+    sliderRight:'页面向右渐出',
+    sliderDown:'页面向下渐出',
+    sliderUp:'页面向上渐出',
+  },
+  tags: {
+    refresh: '刷新',
+    fullscreen: '全屏',
+    closeRightTag: '关闭右侧标签',
+    closeLeftTag: '关闭左侧标签',
+    closeTag: '关闭当前标签',
+    closeOtherTag: '关闭其他标签',
+  },
+  noticeTitle: '系统提示',
+  save: '保存',
+  cancel: '取消',
+}

+ 8 - 0
src/i18n/zh_CN/upload.js

@@ -0,0 +1,8 @@
+export default {
+  fileHashFail: '获取文件Hash失败,请重试',
+  sizeLimit: '文件大小超过了限制',
+  uploadFailed: '文件上传失败',
+  buttonText: '本地上传',
+  clickUpload: '点击上传',
+  uploadDesc: '将文件拖到此处,或',
+}

+ 4 - 0
src/i18n/zh_CN/user.js

@@ -0,0 +1,4 @@
+export default {
+  name: '菜单管理',
+  'system:cache': '系统缓存'
+}

+ 19 - 0
src/layout/404.vue

@@ -0,0 +1,19 @@
+
+<template>
+  <div class="page max-7xl mx-auto text-center">
+    <div class="bg mx-auto">
+      <img src="@/assets/404.svg" />
+      <div class="mt-2">{{ $t('sys.notFoundPage') }}</div>
+    </div>
+    <div class="mt-5"><a-button type="primary" @click="$router.push({ name: 'dashboard' })">{{ $t('sys.goHome') }}</a-button></div>
+  </div>
+</template>
+
+<style scoped lang="less">
+.page {
+    position: absolute; top: 50%; left: 50%; margin-top: -200px; margin-left: -195px;
+}
+.bg, .bg img{
+   width: 390px;
+}
+</style>

+ 62 - 0
src/layout/components/banner/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <a-layout class="layout flex flex-col h-full">
+    <a-layout-header class="ma-ui-header flex justify-between h-50 layout-banner-header operation-area">
+      <div class="flex justify-between md:justify-center logo">
+        <a-avatar class="mt-1 ml-2 md:ml-0" :size="40"><img :src="`${$url}logo.png`" class="bg-white" /></a-avatar>
+        <span class="ml-2 text-xl mt-2.5 hidden md:block">{{ $title }}</span>
+      </div>
+      <div class="flex justify-between w-full layout-banner">
+        <children-banner v-model="userStore.routers" />
+        <ma-operation />
+      </div>
+    </a-layout-header>
+    <ma-tags class="hidden lg:flex ma-ui-tags" />
+    <ma-worker-area />
+  </a-layout>
+</template>
+
+<script setup>
+  import { ref, watch, onMounted } from 'vue'
+  import { useAppStore, useUserStore } from '@/store'
+  import { useRoute } from 'vue-router'
+  import MaOperation from '../ma-operation.vue'
+  import MaWorkerArea from '../ma-workerArea.vue'
+  import MaTags from '../ma-tags.vue'
+  import ChildrenBanner from '../components/children-banner.vue'
+
+  const route = useRoute()
+
+  const MaMenuRef = ref(null)
+  const userStore = useUserStore()
+  const appStore = useAppStore()
+  const actives = ref([])
+
+  onMounted(() => {
+    actives.value = [ route.name ]
+  })
+
+  watch(() => route, v => {
+    actives.value = [ v.name ]
+  }, { deep: true })
+</script>
+
+<style scoped lang="less">
+.tags {
+  margin-top: -1px;
+}
+:deep(.arco-menu-collapse-button) {
+  right: 10px;
+}
+:deep(.layout-banner .arco-menu-horizontal .arco-menu-inner) {
+  align-items: none;
+  padding: 8px 10px;
+  overflow-y: hidden;
+}
+:deep(.sys-menus .arco-menu-icon svg) {
+  display: inline;
+  margin-bottom: -1px;
+}
+:deep(.sys-menus .arco-menu-icon .iconify-icon) {
+  margin-bottom: 2px;
+}
+</style>

+ 21 - 0
src/layout/components/classic/index.vue

@@ -0,0 +1,21 @@
+<template>
+  <a-layout class="layout flex justify-between h-full">
+
+    <ma-classic-slider class="ma-ui-slider" />
+
+    <a-layout-content class="flex flex-col">
+      <ma-classic-header class="ma-ui-header" />
+      <ma-worker-area />
+    </a-layout-content>
+
+  </a-layout>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+
+import MaClassicSlider from './ma-classic-slider.vue'
+import MaClassicHeader from './ma-classic-header.vue'
+import MaWorkerArea from '../ma-workerArea.vue'
+
+</script>

+ 16 - 0
src/layout/components/classic/ma-classic-header.vue

@@ -0,0 +1,16 @@
+<template>
+  <a-layout-header class="layout-classic-header flex flex-col operation-area">
+    <div class="flex justify-between layout-classic-header-container">
+      <a-avatar class="mt-1 ml-2 inline lg:hidden" style="width:45px;" :size="40"><img :src="`${$url}logo.png`" class="bg-white" /></a-avatar>
+      <ma-breadcrumb />
+      <ma-operation />
+    </div>
+    <ma-tags class="hidden lg:flex" />
+  </a-layout-header>
+</template>
+
+<script setup>
+  import MaBreadcrumb from '../ma-breadcrumb.vue'
+  import MaOperation from '../ma-operation.vue'
+  import MaTags from '../ma-tags.vue'
+</script>

+ 37 - 0
src/layout/components/classic/ma-classic-slider.vue

@@ -0,0 +1,37 @@
+<template>
+  <a-layout-sider
+    class="layout-classic-sider h-full flex flex-col hidden lg:block"
+    :style="`width: ${appStore.menuCollapse ? '48px' : appStore.menuWidth + 'px'};`"
+  >
+    <div class="flex justify-center logo">
+      <a-avatar class="mt-1" :size="40"><img :src="`${$url}logo.png`" class="bg-white" /></a-avatar>
+      <span class="ml-2 text-xl mt-2.5" v-if="! appStore.menuCollapse">{{ $title }}</span>
+    </div>
+    <ma-menu
+      ref="MaMenuRef"
+      height="calc(100% - 51px)"
+      :class="`${appStore.menuCollapse ? 'ml-1.5' : ''};`"
+    />
+  </a-layout-sider>
+</template>
+
+<script setup>
+  import { ref, onMounted } from 'vue'
+  import { useAppStore, useUserStore } from '@/store'
+  import MaMenu from '../ma-menu.vue'
+
+
+  const MaMenuRef = ref(null)
+  const userStore = useUserStore()
+  const appStore = useAppStore()
+
+  onMounted(() => {
+    setTimeout(_ => {
+      MaMenuRef.value.menus = userStore.routers
+    }, 50)
+  })
+</script>
+
+<style>
+.logo { height: 51px; border-bottom: 1px solid var(--color-border-1); }
+</style>

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