Seven 6 月之前
父節點
當前提交
99a9b2d2af
共有 100 個文件被更改,包括 2608 次插入0 次删除
  1. 14 0
      admin-ui/.editorconfig
  2. 5 0
      admin-ui/.env
  3. 11 0
      admin-ui/.env.development
  4. 15 0
      admin-ui/.env.production
  5. 16 0
      admin-ui/.env.staging
  6. 10 0
      admin-ui/.eslintignore
  7. 130 0
      admin-ui/.eslintrc.cjs
  8. 22 0
      admin-ui/.gitignore
  9. 11 0
      admin-ui/.markdownlint.json
  10. 3 0
      admin-ui/.npmrc
  11. 9 0
      admin-ui/.prettierrc.js
  12. 4 0
      admin-ui/.stylelintignore
  13. 21 0
      admin-ui/LICENSE
  14. 9 0
      admin-ui/README.md
  15. 55 0
      admin-ui/build/cdn.ts
  16. 63 0
      admin-ui/build/compress.ts
  17. 32 0
      admin-ui/build/index.ts
  18. 54 0
      admin-ui/build/info.ts
  19. 31 0
      admin-ui/build/optimize.ts
  20. 51 0
      admin-ui/build/plugins.ts
  21. 34 0
      admin-ui/build/utils.ts
  22. 87 0
      admin-ui/index.html
  23. 138 0
      admin-ui/package.json
  24. 12 0
      admin-ui/postcss.config.js
  25. 二進制
      admin-ui/public/favicon.ico
  26. 1 0
      admin-ui/public/logo.svg
  27. 22 0
      admin-ui/public/serverConfig.json
  28. 17 0
      admin-ui/src/App.vue
  29. 22 0
      admin-ui/src/api/auth/index.ts
  30. 9 0
      admin-ui/src/api/auth/routes.ts
  31. 52 0
      admin-ui/src/api/base.ts
  32. 24 0
      admin-ui/src/api/monitor/cache.ts
  33. 77 0
      admin-ui/src/api/monitor/quartz.ts
  34. 51 0
      admin-ui/src/api/sys/config.ts
  35. 41 0
      admin-ui/src/api/sys/permission.ts
  36. 66 0
      admin-ui/src/api/sys/role.ts
  37. 83 0
      admin-ui/src/api/sys/user.ts
  38. 26 0
      admin-ui/src/assets/iconfont/iconfont.css
  39. 1 0
      admin-ui/src/assets/iconfont/iconfont.js
  40. 30 0
      admin-ui/src/assets/iconfont/iconfont.json
  41. 二進制
      admin-ui/src/assets/iconfont/iconfont.ttf
  42. 二進制
      admin-ui/src/assets/iconfont/iconfont.woff
  43. 二進制
      admin-ui/src/assets/iconfont/iconfont.woff2
  44. 0 0
      admin-ui/src/assets/icons/add.svg
  45. 0 0
      admin-ui/src/assets/icons/chart.svg
  46. 0 0
      admin-ui/src/assets/icons/in.svg
  47. 0 0
      admin-ui/src/assets/icons/income.svg
  48. 0 0
      admin-ui/src/assets/icons/money2.svg
  49. 1 0
      admin-ui/src/assets/icons/order.svg
  50. 0 0
      admin-ui/src/assets/icons/out.svg
  51. 0 0
      admin-ui/src/assets/icons/present.svg
  52. 0 0
      admin-ui/src/assets/icons/tax.svg
  53. 1 0
      admin-ui/src/assets/icons/trx.svg
  54. 1 0
      admin-ui/src/assets/icons/usdt.svg
  55. 1 0
      admin-ui/src/assets/login/avatar.svg
  56. 二進制
      admin-ui/src/assets/login/bg.png
  57. 0 0
      admin-ui/src/assets/login/illustration.svg
  58. 0 0
      admin-ui/src/assets/status/403.svg
  59. 0 0
      admin-ui/src/assets/status/404.svg
  60. 0 0
      admin-ui/src/assets/status/500.svg
  61. 1 0
      admin-ui/src/assets/svg/back_top.svg
  62. 1 0
      admin-ui/src/assets/svg/dark.svg
  63. 1 0
      admin-ui/src/assets/svg/day.svg
  64. 1 0
      admin-ui/src/assets/svg/enter_outlined.svg
  65. 1 0
      admin-ui/src/assets/svg/exit_screen.svg
  66. 1 0
      admin-ui/src/assets/svg/full_screen.svg
  67. 1 0
      admin-ui/src/assets/svg/keyboard_esc.svg
  68. 二進制
      admin-ui/src/assets/user.jpg
  69. 7 0
      admin-ui/src/components/ReAnimateSelector/index.ts
  70. 114 0
      admin-ui/src/components/ReAnimateSelector/src/animate.ts
  71. 127 0
      admin-ui/src/components/ReAnimateSelector/src/index.vue
  72. 5 0
      admin-ui/src/components/ReAuth/index.ts
  73. 20 0
      admin-ui/src/components/ReAuth/src/auth.tsx
  74. 7 0
      admin-ui/src/components/ReBarcode/index.ts
  75. 42 0
      admin-ui/src/components/ReBarcode/src/index.vue
  76. 29 0
      admin-ui/src/components/ReCol/index.ts
  77. 2 0
      admin-ui/src/components/ReCountTo/README.md
  78. 11 0
      admin-ui/src/components/ReCountTo/index.ts
  79. 179 0
      admin-ui/src/components/ReCountTo/src/normal/index.tsx
  80. 31 0
      admin-ui/src/components/ReCountTo/src/normal/props.ts
  81. 72 0
      admin-ui/src/components/ReCountTo/src/rebound/index.tsx
  82. 14 0
      admin-ui/src/components/ReCountTo/src/rebound/props.ts
  83. 77 0
      admin-ui/src/components/ReCountTo/src/rebound/rebound.css
  84. 7 0
      admin-ui/src/components/ReCropper/index.ts
  85. 11 0
      admin-ui/src/components/ReCropper/src/circled.css
  86. 439 0
      admin-ui/src/components/ReCropper/src/index.tsx
  87. 1 0
      admin-ui/src/components/ReCropper/src/svg/arrow-down.svg
  88. 1 0
      admin-ui/src/components/ReCropper/src/svg/arrow-h.svg
  89. 1 0
      admin-ui/src/components/ReCropper/src/svg/arrow-left.svg
  90. 1 0
      admin-ui/src/components/ReCropper/src/svg/arrow-right.svg
  91. 1 0
      admin-ui/src/components/ReCropper/src/svg/arrow-up.svg
  92. 1 0
      admin-ui/src/components/ReCropper/src/svg/arrow-v.svg
  93. 1 0
      admin-ui/src/components/ReCropper/src/svg/change.svg
  94. 1 0
      admin-ui/src/components/ReCropper/src/svg/download.svg
  95. 31 0
      admin-ui/src/components/ReCropper/src/svg/index.ts
  96. 1 0
      admin-ui/src/components/ReCropper/src/svg/reload.svg
  97. 1 0
      admin-ui/src/components/ReCropper/src/svg/rotate-left.svg
  98. 1 0
      admin-ui/src/components/ReCropper/src/svg/rotate-right.svg
  99. 1 0
      admin-ui/src/components/ReCropper/src/svg/search-minus.svg
  100. 1 0
      admin-ui/src/components/ReCropper/src/svg/search-plus.svg

+ 14 - 0
admin-ui/.editorconfig

@@ -0,0 +1,14 @@
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false

+ 5 - 0
admin-ui/.env

@@ -0,0 +1,5 @@
+# 平台本地运行端口号
+VITE_PORT = 8848
+
+# 是否隐藏首页 隐藏 true 不隐藏 false (勿删除,VITE_HIDE_HOME只需在.env文件配置)
+VITE_HIDE_HOME = false

+ 11 - 0
admin-ui/.env.development

@@ -0,0 +1,11 @@
+# 平台本地运行端口号
+VITE_PORT = 8848
+
+# 开发环境读取配置文件路径
+VITE_PUBLIC_PATH = ./
+
+# 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
+VITE_ROUTER_HISTORY = "hash"
+# API访问路径
+ VITE_API_SERVER = "http://admin.gagapay.pro/api"
+#VITE_API_SERVER = "http://www.sortebar.com/api"

+ 15 - 0
admin-ui/.env.production

@@ -0,0 +1,15 @@
+# 线上环境平台打包路径
+VITE_PUBLIC_PATH = ./
+
+# 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
+VITE_ROUTER_HISTORY = "hash"
+
+# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
+VITE_CDN = true
+
+# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件)
+# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
+# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
+VITE_COMPRESSION = "both"
+# API访问路径
+VITE_API_SERVER = "/api"

+ 16 - 0
admin-ui/.env.staging

@@ -0,0 +1,16 @@
+# 预发布也需要生产环境的行为
+# https://cn.vitejs.dev/guide/env-and-mode.html#modes
+# NODE_ENV = development
+
+VITE_PUBLIC_PATH = ./
+
+# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
+VITE_ROUTER_HISTORY = "hash"
+
+# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
+VITE_CDN = true
+
+# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件)
+# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
+# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
+VITE_COMPRESSION = "none"

+ 10 - 0
admin-ui/.eslintignore

@@ -0,0 +1,10 @@
+public
+dist
+*.d.ts
+/src/assets
+package.json
+.eslintrc.cjs
+.prettierrc.js
+postcss.config.js
+tailwind.config.ts
+stylelint.config.js

+ 130 - 0
admin-ui/.eslintrc.cjs

@@ -0,0 +1,130 @@
+// @ts-check
+const { defineConfig } = require("eslint-define-config");
+
+module.exports = defineConfig({
+  root: true,
+  env: {
+    node: true
+  },
+  globals: {
+    // Ref sugar (take 2)
+    $: "readonly",
+    $$: "readonly",
+    $ref: "readonly",
+    $shallowRef: "readonly",
+    $computed: "readonly",
+
+    // index.d.ts
+    // global.d.ts
+    Fn: "readonly",
+    PromiseFn: "readonly",
+    RefType: "readonly",
+    LabelValueOptions: "readonly",
+    EmitType: "readonly",
+    TargetContext: "readonly",
+    ComponentElRef: "readonly",
+    ComponentRef: "readonly",
+    ElRef: "readonly",
+    global: "readonly",
+    ForDataType: "readonly",
+    ComponentRoutes: "readonly",
+
+    // script setup
+    defineProps: "readonly",
+    defineEmits: "readonly",
+    defineExpose: "readonly",
+    withDefaults: "readonly"
+  },
+  extends: [
+    "plugin:vue/vue3-essential",
+    "eslint:recommended",
+    "@vue/typescript/recommended",
+    "@vue/prettier",
+    "@vue/eslint-config-typescript"
+  ],
+  parser: "vue-eslint-parser",
+  parserOptions: {
+    parser: "@typescript-eslint/parser",
+    ecmaVersion: "latest",
+    sourceType: "module",
+    jsxPragma: "React",
+    ecmaFeatures: {
+      jsx: true
+    }
+  },
+  overrides: [
+    {
+      files: ["*.ts", "*.vue"],
+      rules: {
+        "no-undef": "off"
+      }
+    },
+    {
+      files: ["iconfont.js"],
+      rules: {
+        "no-var": "off",
+        "prefer-const": "off"
+      }
+    },
+    {
+      files: ["*.vue"],
+      parser: "vue-eslint-parser",
+      parserOptions: {
+        parser: "@typescript-eslint/parser",
+        extraFileExtensions: [".vue"],
+        ecmaVersion: "latest",
+        ecmaFeatures: {
+          jsx: true
+        }
+      },
+      rules: {
+        "no-undef": "off"
+      }
+    }
+  ],
+  rules: {
+    "vue/no-v-html": "off",
+    "vue/require-default-prop": "off",
+    "vue/require-explicit-emits": "off",
+    "vue/multi-word-component-names": "off",
+    "@typescript-eslint/no-explicit-any": "off", // any
+    "no-debugger": "off",
+    "@typescript-eslint/explicit-module-boundary-types": "off", // setup()
+    "@typescript-eslint/ban-types": "off",
+    "@typescript-eslint/ban-ts-comment": "off",
+    "@typescript-eslint/no-empty-function": "off",
+    "@typescript-eslint/no-non-null-assertion": "off",
+    "vue/html-self-closing": [
+      "error",
+      {
+        html: {
+          void: "always",
+          normal: "always",
+          component: "always"
+        },
+        svg: "always",
+        math: "always"
+      }
+    ],
+    "@typescript-eslint/no-unused-vars": [
+      "error",
+      {
+        argsIgnorePattern: "^_",
+        varsIgnorePattern: "^_"
+      }
+    ],
+    "no-unused-vars": [
+      "error",
+      {
+        argsIgnorePattern: "^_",
+        varsIgnorePattern: "^_"
+      }
+    ],
+    "prettier/prettier": [
+      "error",
+      {
+        endOfLine: "auto"
+      }
+    ]
+  }
+});

+ 22 - 0
admin-ui/.gitignore

@@ -0,0 +1,22 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+.eslintcache
+report.html
+
+pnpm-lock.yaml
+yarn.lock
+npm-debug.log*
+.pnpm-error.log*
+.pnpm-debug.log
+tests/**/coverage/
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+tsconfig.tsbuildinfo

+ 11 - 0
admin-ui/.markdownlint.json

@@ -0,0 +1,11 @@
+{
+  "default": true,
+  "MD003": false,
+  "MD033": false,
+  "MD013": false,
+  "MD001": false,
+  "MD025": false,
+  "MD024": false,
+  "MD007": { "indent": 4 },
+  "no-hard-tabs": false
+}

+ 3 - 0
admin-ui/.npmrc

@@ -0,0 +1,3 @@
+shamefully-hoist=true
+strict-peer-dependencies=false
+shell-emulator=true

+ 9 - 0
admin-ui/.prettierrc.js

@@ -0,0 +1,9 @@
+// @ts-check
+
+/** @type {import("prettier").Config} */
+export default {
+  bracketSpacing: true,
+  singleQuote: false,
+  arrowParens: "avoid",
+  trailingComma: "none"
+};

+ 4 - 0
admin-ui/.stylelintignore

@@ -0,0 +1,4 @@
+/dist/*
+/public/*
+public/*
+src/style/reset.scss

+ 21 - 0
admin-ui/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020-present, pure-admin
+
+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.

+ 9 - 0
admin-ui/README.md

@@ -0,0 +1,9 @@
+# Springboot-Admin-3
+
+基于 Vite / Vue3 的前后端分离的后台管理系统
+
+## 特征
+
+- 前端采用 vue-pure-admin (Vue3,Element-Plus,Vite)
+- 支持动态菜单与路由
+- 自定义权限认证与 Security 的结合使用

+ 55 - 0
admin-ui/build/cdn.ts

@@ -0,0 +1,55 @@
+import { Plugin as importToCDN } from "vite-plugin-cdn-import";
+
+/**
+ * @description 打包时采用`cdn`模式,仅限外网使用(默认不采用,如果需要采用cdn模式,请在 .env.production 文件,将 VITE_CDN 设置成true)
+ * 平台采用国内cdn:https://www.bootcdn.cn,当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com
+ * 注意:上面提到的仅限外网使用也不是完全肯定的,如果你们公司内网部署的有相关js、css文件,也可以将下面配置对应改一下,整一套内网版cdn
+ */
+export const cdn = importToCDN({
+  //(prodUrl解释: name: 对应下面modules的name,version: 自动读取本地package.json中dependencies依赖中对应包的版本号,path: 对应下面modules的path,当然也可写完整路径,会替换prodUrl)
+  prodUrl: "https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}",
+  modules: [
+    {
+      name: "vue",
+      var: "Vue",
+      path: "vue.global.prod.min.js"
+    },
+    {
+      name: "vue-router",
+      var: "VueRouter",
+      path: "vue-router.global.min.js"
+    },
+    // 项目中没有直接安装vue-demi,但是pinia用到了,所以需要在引入pinia前引入vue-demi(https://github.com/vuejs/pinia/blob/v2/packages/pinia/package.json#L77)
+    {
+      name: "vue-demi",
+      var: "VueDemi",
+      path: "index.iife.min.js"
+    },
+    {
+      name: "pinia",
+      var: "Pinia",
+      path: "pinia.iife.min.js"
+    },
+    {
+      name: "element-plus",
+      var: "ElementPlus",
+      path: "index.full.min.js",
+      css: "index.min.css"
+    },
+    {
+      name: "axios",
+      var: "axios",
+      path: "axios.min.js"
+    },
+    {
+      name: "dayjs",
+      var: "dayjs",
+      path: "dayjs.min.js"
+    },
+    {
+      name: "echarts",
+      var: "echarts",
+      path: "echarts.min.js"
+    }
+  ]
+});

+ 63 - 0
admin-ui/build/compress.ts

@@ -0,0 +1,63 @@
+import type { Plugin } from "vite";
+import { isArray } from "@pureadmin/utils";
+import compressPlugin from "vite-plugin-compression";
+
+export const configCompressPlugin = (
+  compress: ViteCompression
+): Plugin | Plugin[] => {
+  if (compress === "none") return null;
+
+  const gz = {
+    // 生成的压缩包后缀
+    ext: ".gz",
+    // 体积大于threshold才会被压缩
+    threshold: 0,
+    // 默认压缩.js|mjs|json|css|html后缀文件,设置成true,压缩全部文件
+    filter: () => true,
+    // 压缩后是否删除原始文件
+    deleteOriginFile: false
+  };
+  const br = {
+    ext: ".br",
+    algorithm: "brotliCompress",
+    threshold: 0,
+    filter: () => true,
+    deleteOriginFile: false
+  };
+
+  const codeList = [
+    { k: "gzip", v: gz },
+    { k: "brotli", v: br },
+    { k: "both", v: [gz, br] }
+  ];
+
+  const plugins: Plugin[] = [];
+
+  codeList.forEach(item => {
+    if (compress.includes(item.k)) {
+      if (compress.includes("clear")) {
+        if (isArray(item.v)) {
+          item.v.forEach(vItem => {
+            plugins.push(
+              compressPlugin(Object.assign(vItem, { deleteOriginFile: true }))
+            );
+          });
+        } else {
+          plugins.push(
+            compressPlugin(Object.assign(item.v, { deleteOriginFile: true }))
+          );
+        }
+      } else {
+        if (isArray(item.v)) {
+          item.v.forEach(vItem => {
+            plugins.push(compressPlugin(vItem));
+          });
+        } else {
+          plugins.push(compressPlugin(item.v));
+        }
+      }
+    }
+  });
+
+  return plugins;
+};

+ 32 - 0
admin-ui/build/index.ts

@@ -0,0 +1,32 @@
+/** 处理环境变量 */
+const warpperEnv = (envConf: Recordable): ViteEnv => {
+  /** 此处为默认值 */
+  const ret: ViteEnv = {
+    VITE_PORT: 8848,
+    VITE_PUBLIC_PATH: "",
+    VITE_ROUTER_HISTORY: "",
+    VITE_CDN: false,
+    VITE_API_SERVER: "",
+    VITE_HIDE_HOME: "false",
+    VITE_COMPRESSION: "none"
+  };
+
+  for (const envName of Object.keys(envConf)) {
+    let realName = envConf[envName].replace(/\\n/g, "\n");
+    realName =
+      realName === "true" ? true : realName === "false" ? false : realName;
+
+    if (envName === "VITE_PORT") {
+      realName = Number(realName);
+    }
+    ret[envName] = realName;
+    if (typeof realName === "string") {
+      process.env[envName] = realName;
+    } else if (typeof realName === "object") {
+      process.env[envName] = JSON.stringify(realName);
+    }
+  }
+  return ret;
+};
+
+export { warpperEnv };

+ 54 - 0
admin-ui/build/info.ts

@@ -0,0 +1,54 @@
+import type { Plugin } from "vite";
+import picocolors from "picocolors";
+import { getPackageSize } from "./utils";
+import dayjs, { type Dayjs } from "dayjs";
+import duration from "dayjs/plugin/duration";
+dayjs.extend(duration);
+
+export function viteBuildInfo(): Plugin {
+  let config: { command: string };
+  let startTime: Dayjs;
+  let endTime: Dayjs;
+  let outDir: string;
+  const { green, blue, bold } = picocolors;
+  return {
+    name: "vite:buildInfo",
+    configResolved(resolvedConfig) {
+      config = resolvedConfig;
+      outDir = resolvedConfig.build?.outDir ?? "dist";
+    },
+    buildStart() {
+      console.log(
+        bold(
+          green(
+            `👏欢迎使用${blue(
+              "[vue-pure-admin]"
+            )},如果您感觉不错,记得点击后面链接给个star哦💖 https://github.com/pure-admin/vue-pure-admin`
+          )
+        )
+      );
+      if (config.command === "build") {
+        startTime = dayjs(new Date());
+      }
+    },
+    closeBundle() {
+      if (config.command === "build") {
+        endTime = dayjs(new Date());
+        getPackageSize({
+          folder: outDir,
+          callback: (size: string) => {
+            console.log(
+              bold(
+                green(
+                  `🎉恭喜打包完成(总用时${dayjs
+                    .duration(endTime.diff(startTime))
+                    .format("mm分ss秒")},打包后的大小为${size})`
+                )
+              )
+            );
+          }
+        });
+      }
+    }
+  };
+}

+ 31 - 0
admin-ui/build/optimize.ts

@@ -0,0 +1,31 @@
+/**
+ * 此文件作用于 `vite.config.ts` 的 `optimizeDeps.include` 依赖预构建配置项
+ * 依赖预构建,`vite` 启动时会将下面 include 里的模块,编译成 esm 格式并缓存到 node_modules/.vite 文件夹,页面加载到对应模块时如果浏览器有缓存就读取浏览器缓存,如果没有会读取本地缓存并按需加载
+ * 尤其当您禁用浏览器缓存时(这种情况只应该发生在调试阶段)必须将对应模块加入到 include里,否则会遇到开发环境切换页面卡顿的问题(vite 会认为它是一个新的依赖包会重新加载并强制刷新页面),因为它既无法使用浏览器缓存,又没有在本地 node_modules/.vite 里缓存
+ * 温馨提示:如果您使用的第三方库是全局引入,也就是引入到 src/main.ts 文件里,就不需要再添加到 include 里了,因为 vite 会自动将它们缓存到 node_modules/.vite
+ */
+const include = [
+  "qs",
+  "mitt",
+  "dayjs",
+  "axios",
+  "pinia",
+  "js-cookie",
+  "sortablejs",
+  "pinyin-pro",
+  "@vueuse/core",
+  "@pureadmin/utils",
+  "responsive-storage"
+];
+
+/**
+ * 在预构建中强制排除的依赖项
+ * 温馨提示:所有以 `@iconify-icons/` 开头引入的的本地图标模块,都应该加入到下面的 `exclude` 里,因为平台推荐的使用方式是哪里需要哪里引入而且都是单个的引入,不需要预构建,直接让浏览器加载就好
+ */
+const exclude = [
+  "@iconify-icons/ep",
+  "@iconify-icons/ri",
+  "@pureadmin/theme/dist/browser-utils"
+];
+
+export { include, exclude };

+ 51 - 0
admin-ui/build/plugins.ts

@@ -0,0 +1,51 @@
+import { cdn } from "./cdn";
+import vue from "@vitejs/plugin-vue";
+import { viteBuildInfo } from "./info";
+import svgLoader from "vite-svg-loader";
+import type { PluginOption } from "vite";
+import vueJsx from "@vitejs/plugin-vue-jsx";
+import { configCompressPlugin } from "./compress";
+import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
+import { visualizer } from "rollup-plugin-visualizer";
+import removeConsole from "vite-plugin-remove-console";
+import { themePreprocessorPlugin } from "@pureadmin/theme";
+import { genScssMultipleScopeVars } from "../src/layout/theme";
+import path from "path";
+
+export function getPluginsList(
+  command: string,
+  VITE_CDN: boolean,
+  VITE_COMPRESSION: ViteCompression
+): PluginOption[] {
+  const lifecycle = process.env.npm_lifecycle_event;
+  return [
+    vue(),
+    // jsx、tsx语法支持
+    vueJsx(),
+    VITE_CDN ? cdn : null,
+    configCompressPlugin(VITE_COMPRESSION),
+    // 线上环境删除console
+    removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }),
+    viteBuildInfo(),
+    // 自定义主题
+    themePreprocessorPlugin({
+      scss: {
+        multipleScopeVars: genScssMultipleScopeVars(),
+        extract: true
+      }
+    }),
+    // svg组件化支持
+    svgLoader(),
+    // ElementPlus({}),
+    createSvgIconsPlugin({
+      // 指定需要缓存的图标文件夹
+      iconDirs: [path.resolve(process.cwd(), "src/assets/icons")],
+      // 指定symbolId格式
+      symbolId: "icon-[dir]-[name]"
+    }),
+    // 打包分析
+    lifecycle === "report"
+      ? visualizer({ open: true, brotliSize: true, filename: "report.html" })
+      : null
+  ];
+}

+ 34 - 0
admin-ui/build/utils.ts

@@ -0,0 +1,34 @@
+import { readdir, stat } from "node:fs";
+import { sum, formatBytes } from "@pureadmin/utils";
+
+const fileListTotal: number[] = [];
+
+/**
+ * @description 获取指定文件夹中所有文件的总大小
+ */
+export const getPackageSize = options => {
+  const { folder = "dist", callback, format = true } = options;
+  readdir(folder, (err, files: string[]) => {
+    if (err) throw err;
+    let count = 0;
+    const checkEnd = () => {
+      ++count == files.length &&
+        callback(format ? formatBytes(sum(fileListTotal)) : sum(fileListTotal));
+    };
+    files.forEach((item: string) => {
+      stat(`${folder}/${item}`, async (err, stats) => {
+        if (err) throw err;
+        if (stats.isFile()) {
+          fileListTotal.push(stats.size);
+          checkEnd();
+        } else if (stats.isDirectory()) {
+          getPackageSize({
+            folder: `${folder}/${item}/`,
+            callback: checkEnd
+          });
+        }
+      });
+    });
+    files.length === 0 && callback(0);
+  });
+};

+ 87 - 0
admin-ui/index.html

@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="renderer" content="webkit" />
+    <meta
+      name="viewport"
+      content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
+    />
+    <title>pure-admin-thin</title>
+    <link rel="icon" href="/favicon.ico" />
+    <script>
+      window.process = {};
+    </script>
+  </head>
+
+  <body>
+    <div id="app">
+      <style>
+        html,
+        body,
+        #app {
+          position: relative;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 100%;
+          height: 100%;
+          overflow: hidden;
+        }
+
+        .loader,
+        .loader::before,
+        .loader::after {
+          width: 2.5em;
+          height: 2.5em;
+          border-radius: 50%;
+          animation: load-animation 1.8s infinite ease-in-out;
+          animation-fill-mode: both;
+        }
+
+        .loader {
+          position: relative;
+          top: 0;
+          margin: 80px auto;
+          font-size: 10px;
+          color: #406eeb;
+          text-indent: -9999em;
+          transform: translateZ(0);
+          transform: translate(-50%, 0);
+          animation-delay: -0.16s;
+        }
+
+        .loader::before,
+        .loader::after {
+          position: absolute;
+          top: 0;
+          content: "";
+        }
+
+        .loader::before {
+          left: -3.5em;
+          animation-delay: -0.32s;
+        }
+
+        .loader::after {
+          left: 3.5em;
+        }
+
+        @keyframes load-animation {
+          0%,
+          80%,
+          100% {
+            box-shadow: 0 2.5em 0 -1.3em;
+          }
+
+          40% {
+            box-shadow: 0 2.5em 0 0;
+          }
+        }
+      </style>
+      <div class="loader"></div>
+    </div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 138 - 0
admin-ui/package.json

@@ -0,0 +1,138 @@
+{
+  "name": "pure-admin-thin",
+  "version": "4.5.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
+    "serve": "pnpm dev",
+    "build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build",
+    "build:staging": "rimraf dist && vite build --mode staging",
+    "report": "rimraf dist && vite build",
+    "preview": "vite preview",
+    "preview:build": "pnpm build && vite preview",
+    "typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
+    "svgo": "svgo -f src/assets/svg -o src/assets/svg",
+    "cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
+    "clean:cache": "rimraf node_modules && rimraf .eslintcache && pnpm install",
+    "lint:eslint": "eslint --cache --max-warnings 0  \"{src,build}/**/*.{vue,js,ts,tsx}\" --fix",
+    "lint:prettier": "prettier --write  \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
+    "lint:stylelint": "stylelint \"**/*.{html,vue,css,scss}\" --fix --cache --cache-location node_modules/.cache/stylelint/",
+    "lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
+    "lint:pretty": "pretty-quick --staged",
+    "lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
+    "prepare": "husky install",
+    "preinstall": "npx only-allow pnpm"
+  },
+  "browserslist": [
+    "> 1%",
+    "not ie 11",
+    "not op_mini all"
+  ],
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
+    "@pureadmin/descriptions": "^1.2.0",
+    "@pureadmin/table": "^3.0.2",
+    "@pureadmin/utils": "^2.4.4",
+    "@types/jsrsasign": "^10.5.8",
+    "@vueuse/core": "^10.7.2",
+    "@vueuse/motion": "^2.0.0",
+    "animate.css": "^4.1.1",
+    "axios": "^1.6.3",
+    "dayjs": "^1.11.9",
+    "echarts": "^5.4.3",
+    "element-plus": "^2.5.6",
+    "js-cookie": "^3.0.5",
+    "jsrsasign": "^10.8.6",
+    "jsrsasign-util": "^1.0.5",
+    "mitt": "^3.0.1",
+    "nprogress": "^0.2.0",
+    "path": "^0.12.7",
+    "pinia": "^2.1.6",
+    "pinyin-pro": "^3.18.2",
+    "qs": "^6.11.2",
+    "responsive-storage": "^2.2.0",
+    "sortablejs": "^1.15.0",
+    "typeit": "^8.8.0",
+    "vue": "^3.4.15",
+    "vue-clipboard3": "^2.0.0",
+    "vue-router": "^4.2.4",
+    "vue-types": "^5.1.1",
+    "vue3-json-viewer": "^2.2.2"
+  },
+  "devDependencies": {
+    "@iconify-icons/ep": "^1.2.12",
+    "@iconify-icons/ri": "^1.2.10",
+    "@iconify/vue": "^4.1.1",
+    "@pureadmin/theme": "^3.2.0",
+    "@types/js-cookie": "^3.0.3",
+    "@types/node": "^20.5.3",
+    "@types/nprogress": "0.2.0",
+    "@types/qs": "^6.9.7",
+    "@types/sortablejs": "^1.15.1",
+    "@typescript-eslint/eslint-plugin": "^6.4.1",
+    "@typescript-eslint/parser": "^6.4.1",
+    "@vitejs/plugin-vue": "^4.5.0",
+    "@vitejs/plugin-vue-jsx": "^3.1.0",
+    "@vue/eslint-config-prettier": "^7.1.0",
+    "@vue/eslint-config-typescript": "^11.0.3",
+    "autoprefixer": "^10.4.15",
+    "cloc": "^2.11.0",
+    "cssnano": "^6.0.1",
+    "eslint": "^8.43.0",
+    "eslint-define-config": "^2.1.0",
+    "eslint-plugin-prettier": "^5.0.0",
+    "eslint-plugin-vue": "^9.15.1",
+    "husky": "^8.0.3",
+    "lint-staged": "^13.2.2",
+    "picocolors": "^1.0.0",
+    "postcss": "^8.4.31",
+    "postcss-html": "^1.5.0",
+    "postcss-import": "^15.1.0",
+    "postcss-scss": "^4.0.6",
+    "prettier": "^3.2.4",
+    "pretty-quick": "^4.0.0",
+    "rimraf": "^5.0.1",
+    "rollup-plugin-visualizer": "^5.9.2",
+    "sass": "^1.66.1",
+    "sass-loader": "^13.3.2",
+    "stylelint": "^15.10.3",
+    "stylelint-config-html": "^1.1.0",
+    "stylelint-config-recess-order": "^4.3.0",
+    "stylelint-config-recommended": "^13.0.0",
+    "stylelint-config-recommended-scss": "^12.0.0",
+    "stylelint-config-recommended-vue": "^1.5.0",
+    "stylelint-config-standard": "^34.0.0",
+    "stylelint-config-standard-scss": "^10.0.0",
+    "stylelint-order": "^6.0.3",
+    "stylelint-prettier": "^4.0.2",
+    "stylelint-scss": "^5.1.0",
+    "svgo": "^3.0.2",
+    "tailwindcss": "^3.3.3",
+    "terser": "^5.19.2",
+    "typescript": "5.1.6",
+    "vite": "^5.1.3",
+    "vite-plugin-cdn-import": "^0.3.5",
+    "vite-plugin-compression": "^0.5.1",
+    "vite-plugin-remove-console": "^2.1.1",
+    "vite-plugin-svg-icons": "^2.0.1",
+    "vite-svg-loader": "^4.0.0",
+    "vue-eslint-parser": "^9.3.1",
+    "vue-tsc": "^1.8.8"
+  },
+  "pnpm": {
+    "peerDependencyRules": {
+      "ignoreMissing": [
+        "rollup",
+        "webpack",
+        "core-js"
+      ]
+    },
+    "allowedDeprecatedVersions": {
+      "sourcemap-codec": "*",
+      "w3c-hr-time": "*",
+      "stable": "*"
+    }
+  },
+  "license": "MIT"
+}

+ 12 - 0
admin-ui/postcss.config.js

@@ -0,0 +1,12 @@
+// @ts-check
+
+/** @type {import('postcss-load-config').Config} */
+export default {
+  plugins: {
+    "postcss-import": {},
+    "tailwindcss/nesting": {},
+    tailwindcss: {},
+    autoprefixer: {},
+    ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
+  }
+};

二進制
admin-ui/public/favicon.ico


+ 1 - 0
admin-ui/public/logo.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109z"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665z"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.114 323.114 0 0 1-107.769-242.852z"/></svg>

+ 22 - 0
admin-ui/public/serverConfig.json

@@ -0,0 +1,22 @@
+{
+  "Version": "4.4.0",
+  "Title": "管理系统",
+  "FixedHeader": true,
+  "HiddenSideBar": false,
+  "MultiTagsCache": false,
+  "KeepAlive": true,
+  "Layout": "vertical",
+  "Theme": "default",
+  "DarkMode": false,
+  "Grey": false,
+  "Weak": false,
+  "HideTabs": false,
+  "SidebarStatus": true,
+  "EpThemeColor": "#409EFF",
+  "ShowLogo": true,
+  "ShowModel": "smart",
+  "MenuArrowIconNoTransition": true,
+  "CachingAsyncRoutes": false,
+  "TooltipEffect": "light",
+  "ResponsiveStorageNameSpace": "responsive-"
+}

+ 17 - 0
admin-ui/src/App.vue

@@ -0,0 +1,17 @@
+<template>
+  <el-config-provider :locale="currentLocale">
+    <router-view />
+    <ReDialog />
+  </el-config-provider>
+</template>
+
+<script setup lang="ts">
+import { computed } from "vue";
+import { ElConfigProvider } from "element-plus";
+import zhCn from "element-plus/dist/locale/zh-cn.mjs";
+import { ReDialog } from "@/components/ReDialog";
+
+const currentLocale = computed(() => {
+  return zhCn;
+});
+</script>

+ 22 - 0
admin-ui/src/api/auth/index.ts

@@ -0,0 +1,22 @@
+import { RSA } from "@/utils/crypto";
+import { post, Result } from "../base";
+
+/**
+ * 用户登录换取token
+ * @param username 用户名
+ * @param password 密码
+ */
+export function login(username: string, password: string) {
+  return post<any, Result<any>>("/auth/login", {
+    username: username,
+    password: RSA.encrypt(password)
+  });
+}
+/**
+ *  登出
+ *
+ * @returns .
+ */
+export function logout() {
+  return post<any, Result<any>>("/auth/logout", {});
+}

+ 9 - 0
admin-ui/src/api/auth/routes.ts

@@ -0,0 +1,9 @@
+import { get, Result } from "../base";
+
+/**
+ * 当前用户的路由信息
+ * @returns .
+ */
+export function getRoutes(): Promise<Result<any>> {
+  return get<any, Result<any>>("/sys/permission/routes", {});
+}

+ 52 - 0
admin-ui/src/api/base.ts

@@ -0,0 +1,52 @@
+import { http } from "@/utils/http";
+export interface Result<T> {
+  code: number;
+  message: string;
+  result: T;
+  success: boolean;
+  timestamp: number;
+}
+export interface BaseQuery {
+  size?: number;
+  current?: number;
+}
+/**
+ * post
+ */
+export function post<T, P>(url: string, data: T): Promise<Result<P>> {
+  return http.post<T, Result<P>>(url, { data: data });
+}
+/**
+ * get
+ */
+export function get<T, P>(url: string, params?: T): Promise<Result<P>> {
+  return http.get<T, Result<P>>(url, { params: params });
+}
+/**
+ * put
+ * @param url path
+ * @param params query
+ * @param data body
+ * @returns .
+ */
+export function put<T>(
+  url: string,
+  params?: any,
+  data?: T
+): Promise<Result<T>> {
+  return http.request("put", url, { params: params, data: data });
+}
+/**
+ * 删除
+ * @param url .
+ * @param params .
+ * @param data .
+ * @returns .
+ */
+export function deleteRequest<T>(
+  url: string,
+  params?: any,
+  data?: any
+): Promise<Result<T>> {
+  return http.request("delete", url, { params: params, data: data });
+}

+ 24 - 0
admin-ui/src/api/monitor/cache.ts

@@ -0,0 +1,24 @@
+import { deleteRequest, get } from "../base";
+/**
+ * 列表查询
+ * @returns .
+ */
+export const queryList = () => {
+  return get("/monitor/cache");
+};
+/**
+ * 查找详情
+ * @param param .
+ * @returns .
+ */
+export const queryInfo = (param: any) => {
+  return get("/monitor/cache/query", param);
+};
+/**
+ * 移除
+ * @param param .
+ * @returns .
+ */
+export const remove = (param: any) => {
+  return deleteRequest("/monitor/cache/remove", {}, param);
+};

+ 77 - 0
admin-ui/src/api/monitor/quartz.ts

@@ -0,0 +1,77 @@
+import { get, Result, post, put, deleteRequest } from "../base";
+
+/**
+ * 分页查询
+ * @param query .
+ * @returns .
+ */
+export function queryPage<T>(query?: any): Promise<Result<T>> {
+  return get("/monitor/quartz/job/query/page", query);
+}
+/**
+ * 列表查询
+ * @param query .
+ * @returns .
+ */
+export function queryList<T>(query?: any): Promise<Result<T>> {
+  return get("/monitor/quartz/job/query/list", query);
+}
+/**
+ *  保存
+ * @param data .
+ * @returns  .
+ */
+export function save<T>(data?: T): Promise<Result<T>> {
+  return post("/monitor/quartz/job/save", data);
+}
+/**
+ * 更新
+ * @param id .
+ * @param data .
+ * @returns .
+ */
+export function update<T>(id: string, data?: T): Promise<Result<T>> {
+  return put(`/monitor/quartz/job/update/${id}`, {}, data);
+}
+/**
+ * 删除
+ * @param id .
+ * @returns .
+ */
+export function delByIds<T>(id: string[]): Promise<Result<T>> {
+  return deleteRequest(`/monitor/quartz/job/delete`, {}, id);
+}
+/**
+ * 检查cron表达式
+ * @param cron .
+ * @returns .
+ */
+export function checkCorn<T>(cron: string): Promise<Result<T>> {
+  return get(`/monitor/quartz/job/check/cron`, { cron: cron });
+}
+
+/**
+ * 启动
+ * @param id .
+ * @returns .
+ */
+export function resume<T>(id: string): Promise<Result<T>> {
+  return put(`/monitor/quartz/job/resume/${id}`);
+}
+
+/**
+ * 立即执行
+ * @param id .
+ * @returns .
+ */
+export function execute<T>(id: string): Promise<Result<T>> {
+  return put(`/monitor/quartz/job/execute/${id}`);
+}
+/**
+ * 根据ID删除
+ * @param id .
+ * @returns .
+ */
+export function delById<T>(id: string): Promise<Result<T>> {
+  return deleteRequest(`/monitor/quartz/job/delete`, { id: id }, {});
+}

+ 51 - 0
admin-ui/src/api/sys/config.ts

@@ -0,0 +1,51 @@
+import { get, Result, post, put, deleteRequest } from "../base";
+
+/**
+ * 分页查询
+ * @param query .
+ * @returns .
+ */
+export function queryPage<T>(query?: any): Promise<Result<T>> {
+  return get("/sys/config/query/page", query);
+}
+/**
+ * 列表查询
+ * @param query .
+ * @returns .
+ */
+export function queryList<T>(query?: any): Promise<Result<T>> {
+  return get("/sys/config/query/list", query);
+}
+/**
+ *  保存
+ * @param data .
+ * @returns  .
+ */
+export function save<T>(data?: T): Promise<Result<T>> {
+  return post("/sys/config/save", data);
+}
+/**
+ * 更新
+ * @param id .
+ * @param data .
+ * @returns .
+ */
+export function update<T>(id: string, data?: T): Promise<Result<T>> {
+  return put(`/sys/config/update/${id}`, {}, data);
+}
+/**
+ * 删除
+ * @param id .
+ * @returns .
+ */
+export function delByIds<T>(id: string[]): Promise<Result<T>> {
+  return deleteRequest(`/sys/config/delete`, {}, id);
+}
+/**
+ * 刷新缓存
+ * @param id .
+ * @returns .
+ */
+export function refreshCache<T>(id: string): Promise<Result<T>> {
+  return put(`/sys/config/refresh/${id}`, {});
+}

+ 41 - 0
admin-ui/src/api/sys/permission.ts

@@ -0,0 +1,41 @@
+import { BaseQuery, deleteRequest, get, post, put, Result } from "../base";
+
+/**
+ *  菜单与权限树形列表
+ * @param query 查询参数
+ * @returns 树形列表
+ */
+export function treeList<P extends BaseQuery, T>(
+  query?: P
+): Promise<Result<T>> {
+  return get("/sys/permission/tree", query);
+}
+/**
+ * 菜单树形列表
+ * @param query .
+ */
+export function treeMenus<P extends BaseQuery, T>(
+  query?: P
+): Promise<Result<T>> {
+  return get("/sys/permission/tree/menu", query);
+}
+/**
+ * 新增
+ * @param data .
+ * @returns .
+ */
+export function savePermission<T>(data: any): Promise<Result<T>> {
+  return post("/sys/permission/save", data);
+}
+/**
+ *  更新
+ * @param id .
+ * @param data .
+ * @returns  .
+ */
+export function updatePermission<T>(id: string, data: any): Promise<Result<T>> {
+  return put("/sys/permission/update", { id: id }, data);
+}
+export function deletePermission<T>(ids: string[]): Promise<Result<T>> {
+  return deleteRequest("/sys/permission/delete", {}, ids);
+}

+ 66 - 0
admin-ui/src/api/sys/role.ts

@@ -0,0 +1,66 @@
+import { get, Result, post, put } from "../base";
+
+/**
+ * 分页查询角色
+ * @param query .
+ * @returns .
+ */
+export function roleQueryPage<T>(query?: any): Promise<Result<T>> {
+  return get("/sys/role/query/page", query);
+}
+/**
+ * 查询角色列表
+ * @param query .
+ * @returns .
+ */
+export function roleQueryList<T>(query?: any): Promise<Result<T>> {
+  return get("/sys/role/query/list", query);
+}
+/**
+ *  检查code是否重复
+ * @param code code
+ * @param id 需要排查的ID
+ * @returns .
+ */
+export function checkCode<T>(code: string, id?: string): Promise<Result<T>> {
+  const _data: any = { code: code };
+  if (id) {
+    _data.id = id;
+  }
+  return get("/sys/role/check/code", _data);
+}
+/**
+ *  保存角色
+ * @param data .
+ * @returns  .
+ */
+export function roleSave<T>(data?: T): Promise<Result<T>> {
+  return post("/sys/role/save", data);
+}
+/**
+ * 根据ID更新角色
+ * @param id 角色ID
+ * @param data 需要更新的信息
+ * @returns .
+ */
+export function roleUpdate<T>(id: string, data?: T): Promise<Result<T>> {
+  return put("/sys/role/update", { id: id }, data);
+}
+/**
+ * 角色权限
+ * @param id  角色ID
+ * @returns .
+ */
+export function queryPermission(id: string): Promise<Result<string[]>> {
+  return get("/sys/role/permission", { id: id });
+}
+/**
+ * 更新权限
+ * @param id 角色ID
+ * @param permissionIds 权限ID
+ * @returns .
+ */
+export function updatePermission(id: string, permissionIds?: string[]) {
+  permissionIds = permissionIds || [];
+  return put("/sys/role/permission", { id: id }, permissionIds);
+}

+ 83 - 0
admin-ui/src/api/sys/user.ts

@@ -0,0 +1,83 @@
+import { Result, deleteRequest, get, post, put } from "../base";
+/**
+ * 分页查询
+ * @param query .
+ * @returns .
+ */
+export function queryPage<T>(query?: any): Promise<Result<T>> {
+  return get("/sys/user/query/page", query);
+}
+/**
+ * 列表查询
+ * @param query .
+ * @returns .
+ */
+export function queryList<T>(query?: any): Promise<Result<T>> {
+  return get("/sys/user/query/list", query);
+}
+/**
+ * 检查用户名是否存在
+ * @param username .
+ * @returns .
+ */
+export function hashUsername(username: string): Promise<Result<Boolean>> {
+  return get("/sys/user/check/username", { username: username });
+}
+/**
+ * 新增
+ * @param data .
+ * @returns .
+ */
+export function save<T>(data: any): Promise<Result<T>> {
+  return post("/sys/user/save", data);
+}
+/**
+ * 更新
+ * @param id .
+ * @param data .
+ * @returns  .
+ */
+export function update<T>(id: string, data: any): Promise<Result<T>> {
+  return put(`/sys/user/update/${id}`, {}, data);
+}
+
+/**
+ * 获取用户角色ID
+ * @param userId .
+ * @returns .
+ */
+export function queryRoleIds<T>(userId: string): Promise<Result<T>> {
+  return get(`/sys/user/query/role/ids`, { id: userId });
+}
+/**
+ * 删除用户
+ * @param userIds .
+ * @returns  .
+ */
+export function del<T>(userIds: string[]): Promise<Result<T>> {
+  return deleteRequest(`/sys/user/delete`, {}, userIds);
+}
+/**
+ * 重置密码
+ * @param username .
+ * @param data .
+ * @returns .
+ */
+export function resetPasswd<T>(
+  username: string,
+  data: any
+): Promise<Result<T>> {
+  return put(`/sys/user/reset/passwd/${username}`, {}, data);
+}
+/**
+ * 更新密码
+ * @param username .
+ * @param data .
+ * @returns .
+ */
+export function changePasswd<T>(
+  username: string,
+  data: any
+): Promise<Result<T>> {
+  return put(`/sys/user/change/passwd/${username}`, {}, data);
+}

+ 26 - 0
admin-ui/src/assets/iconfont/iconfont.css

@@ -0,0 +1,26 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 2208059 */
+  src: url("iconfont.woff2?t=1671895108120") format("woff2"),
+    url("iconfont.woff?t=1671895108120") format("woff"),
+    url("iconfont.ttf?t=1671895108120") format("truetype");
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.pure-iconfont-tabs:before {
+  content: "\e63e";
+}
+
+.pure-iconfont-logo:before {
+  content: "\e620";
+}
+
+.pure-iconfont-new:before {
+  content: "\e615";
+}

文件差異過大導致無法顯示
+ 1 - 0
admin-ui/src/assets/iconfont/iconfont.js


+ 30 - 0
admin-ui/src/assets/iconfont/iconfont.json

@@ -0,0 +1,30 @@
+{
+  "id": "2208059",
+  "name": "pure-admin",
+  "font_family": "iconfont",
+  "css_prefix_text": "pure-iconfont-",
+  "description": "pure-admin-iconfont",
+  "glyphs": [
+    {
+      "icon_id": "20594647",
+      "name": "Tabs",
+      "font_class": "tabs",
+      "unicode": "e63e",
+      "unicode_decimal": 58942
+    },
+    {
+      "icon_id": "22129506",
+      "name": "PureLogo",
+      "font_class": "logo",
+      "unicode": "e620",
+      "unicode_decimal": 58912
+    },
+    {
+      "icon_id": "7795615",
+      "name": "New",
+      "font_class": "new",
+      "unicode": "e615",
+      "unicode_decimal": 58901
+    }
+  ]
+}

二進制
admin-ui/src/assets/iconfont/iconfont.ttf


二進制
admin-ui/src/assets/iconfont/iconfont.woff


二進制
admin-ui/src/assets/iconfont/iconfont.woff2


文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/icons/add.svg


文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/icons/chart.svg


文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/icons/in.svg


文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/icons/income.svg


文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/icons/money2.svg


+ 1 - 0
admin-ui/src/assets/icons/order.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1686993244114" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8088" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M864 0H160c-35.3 0-64 28.6-64 64v896c0 35.3 28.7 64 64 64h704c35.3 0 64-28.7 64-64V64c0-35.4-28.7-64-64-64zM256 860v-56c0-2.2 1.8-4 4-4h504c2.2 0 4 1.8 4 4v56c0 2.2-1.8 4-4 4H260c-2.2 0-4-1.8-4-4z m512-128c0 2.2-1.8 4-4 4H260c-2.2 0-4-1.8-4-4v-56c0-2.2 1.8-4 4-4h504c2.2 0 4 1.8 4 4v56zM606.3 288H684c2.2 0 4 1.8 4 4v56c0 2.2-1.8 4-4 4H552c-4.4 0-8 3.6-8 8v52c0 2.2 1.8 4 4 4h136c2.2 0 4 1.8 4 4v56c0 2.2-1.8 4-4 4H548c-2.2 0-4 1.8-4 4v88c0 2.2-1.8 4-4 4h-56c-2.2 0-4-1.8-4-4v-88c0-2.2-1.8-4-4-4H340c-2.2 0-4-1.8-4-4v-56c0-2.2 1.8-4 4-4h136c2.2 0 4-1.8 4-4v-52c0-4.4-3.6-8-8-8H340c-2.2 0-4-1.8-4-4v-56c0-2.2 1.8-4 4-4h69.7c3.6 0 5.3-4.3 2.8-6.8L307.2 175.8c-1.6-1.6-1.6-4.1 0-5.7l39.6-39.6c1.6-1.6 4.1-1.6 5.7 0l144.3 144.3c6.2 6.2 16.4 6.2 22.6 0l144.3-144.3c1.6-1.6 4.1-1.6 5.7 0l39.6 39.6c1.6 1.6 1.6 4.1 0 5.7L603.5 281.2c-2.5 2.5-0.8 6.8 2.8 6.8z" p-id="8089" fill="#13227a"></path></svg>

文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/icons/out.svg


文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/icons/present.svg


文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/icons/tax.svg


+ 1 - 0
admin-ui/src/assets/icons/trx.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1686993140687" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5462" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M512.85 511.04m-447.5 0a447.5 447.5 0 1 0 895 0 447.5 447.5 0 1 0-895 0Z" fill="#D80917" p-id="5463"></path><path d="M477.1 787.2c-0.84 0-1.71-0.05-2.55-0.18a18.645 18.645 0 0 1-15.04-12.25L277.69 259.74c-2.31-6.56-0.78-13.86 3.97-18.94s11.96-7.12 18.63-5.23l366.29 102.15c2.37 0.66 4.63 1.8 6.56 3.35l68.87 54.7c7.76 6.15 9.36 17.3 3.64 25.36L492.32 779.31a18.628 18.628 0 0 1-15.22 7.89zM324.8 281.12l157.87 447.25L705 414.01l-52.08-41.37-328.12-91.52z" fill="#FFFFFF" p-id="5464"></path><path d="M477.13 787.2c-0.69 0-1.35-0.04-2.04-0.11-10.23-1.11-17.63-10.31-16.53-20.54l27.42-253.89c1.09-10.27 10.6-17.48 20.54-16.53 10.23 1.11 17.63 10.31 16.53 20.54l-27.42 253.89c-1.02 9.55-9.1 16.64-18.5 16.64z" fill="#FFFFFF" p-id="5465"></path><path d="M504.52 533.31c-4.73 0-9.47-1.78-13.11-5.37-7.32-7.25-7.39-19.05-0.15-26.38L648.3 342.57c7.25-7.32 19.05-7.39 26.37-0.16 7.32 7.25 7.39 19.05 0.15 26.38L517.77 527.77a18.59 18.59 0 0 1-13.25 5.54z" fill="#FFFFFF" p-id="5466"></path><path d="M504.52 533.31c-7.03 0-13.77-4.01-16.93-10.83-4.3-9.34-0.22-20.43 9.1-24.75l225.9-104.28c9.4-4.32 20.43-0.24 24.76 9.12 4.3 9.34 0.22 20.43-9.1 24.75L512.35 531.6a18.857 18.857 0 0 1-7.83 1.71z" fill="#FFFFFF" p-id="5467"></path><path d="M507.21 536.55c-5.46 0-10.85-2.39-14.53-6.99L280.73 265.19c-6.45-8.03-5.15-19.76 2.9-26.2 8.01-6.41 19.79-5.12 26.2 2.9l211.91 264.37c6.45 8.03 5.17 19.76-2.88 26.2a18.563 18.563 0 0 1-11.65 4.09z" fill="#FFFFFF" p-id="5468"></path></svg>

+ 1 - 0
admin-ui/src/assets/icons/usdt.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1686993164133" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5800" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M1023.082985 511.821692c0 281.370746-228.08199 509.452736-509.452736 509.452736-281.360557 0-509.452736-228.08199-509.452737-509.452736 0-281.365652 228.092179-509.452736 509.452737-509.452737 281.370746 0 509.452736 228.087085 509.452736 509.452737" fill="#1BA27A" p-id="5801"></path><path d="M752.731701 259.265592h-482.400796v116.460896h182.969951v171.176119h116.460895v-171.176119h182.96995z" fill="#FFFFFF" p-id="5802"></path><path d="M512.636816 565.13592c-151.358408 0-274.070289-23.954468-274.070289-53.50782 0-29.548259 122.706786-53.507821 274.070289-53.507821 151.358408 0 274.065194 23.959562 274.065194 53.507821 0 29.553353-122.706786 53.507821-274.065194 53.50782m307.734925-44.587303c0-38.107065-137.776398-68.995184-307.734925-68.995184-169.953433 0-307.74002 30.888119-307.74002 68.995184 0 33.557652 106.837333 61.516418 248.409154 67.711363v245.729433h116.450707v-245.632637c142.66205-6.001353 250.615085-34.077294 250.615084-67.808159" fill="#FFFFFF" p-id="5803"></path></svg>

+ 1 - 0
admin-ui/src/assets/login/avatar.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109z"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665z"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.114 323.114 0 0 1-107.769-242.852z"/></svg>

二進制
admin-ui/src/assets/login/bg.png


文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/login/illustration.svg


文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/status/403.svg


文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/status/404.svg


文件差異過大導致無法顯示
+ 0 - 0
admin-ui/src/assets/status/500.svg


+ 1 - 0
admin-ui/src/assets/svg/back_top.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2.88 18.054a35.897 35.897 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0c.166.18.304.332.413.455a35.897 35.897 0 0 1 8.118 15.865c-2.141.451-4.34.747-6.584.874l-2.089 4.178a.5.5 0 0 1-.894 0l-2.089-4.178a44.019 44.019 0 0 1-6.584-.874zm6.698-1.123 1.157.066L12 19.527l1.265-2.53 1.157-.066a42.137 42.137 0 0 0 4.227-.454A33.913 33.913 0 0 0 12 4.09a33.913 33.913 0 0 0-6.649 12.387c1.395.222 2.805.374 4.227.454zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></svg>

+ 1 - 0
admin-ui/src/assets/svg/dark.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.38 2.019a7.5 7.5 0 1 0 10.6 10.6C21.662 17.854 17.316 22 12.001 22 6.477 22 2 17.523 2 12c0-5.315 4.146-9.661 9.38-9.981z"/></svg>

+ 1 - 0
admin-ui/src/assets/svg/day.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85 1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></svg>

+ 1 - 0
admin-ui/src/assets/svg/enter_outlined.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--ant-design" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8z"/></svg>

+ 1 - 0
admin-ui/src/assets/svg/exit_screen.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5l-.5.5zM13 3V1h-1v2.5l.5.5H15V3h-2zm-1 9.5V15h1v-2h2v-1h-2.5l-.5.5zM1 12v1h2v2h1v-2.5l-.5-.5H1zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5v5zM10 7H6v2h4V7z"/></svg>

+ 1 - 0
admin-ui/src/assets/svg/full_screen.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3v8zm2-6h6v4H5V6zM2 6H1V2.5l.5-.5H5v1H2v3zm13-3.5V6h-1V3h-3V2h3.5l.5.5zM14 10h1v3.5l-.5.5H11v-1h3v-3zM2 13h3v1H1.5l-.5-.5V10h1v3z"/></svg>

+ 1 - 0
admin-ui/src/assets/svg/keyboard_esc.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--mdi" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1V7m10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2Z"/></svg>

二進制
admin-ui/src/assets/user.jpg


+ 7 - 0
admin-ui/src/components/ReAnimateSelector/index.ts

@@ -0,0 +1,7 @@
+import reAnimateSelector from "./src/index.vue";
+import { withInstall } from "@pureadmin/utils";
+
+/** [animate.css](https://animate.style/) 选择器组件 */
+export const ReAnimateSelector = withInstall(reAnimateSelector);
+
+export default ReAnimateSelector;

+ 114 - 0
admin-ui/src/components/ReAnimateSelector/src/animate.ts

@@ -0,0 +1,114 @@
+export const animates = [
+  /* Attention seekers  */
+  "bounce",
+  "flash",
+  "pulse",
+  "rubberBand",
+  "shakeX",
+  "headShake",
+  "swing",
+  "tada",
+  "wobble",
+  "jello",
+  "heartBeat",
+  /* Back entrances */
+  "backInDown",
+  "backInLeft",
+  "backInRight",
+  "backInUp",
+  /* Back exits */
+  "backOutDown",
+  "backOutLeft",
+  "backOutRight",
+  "backOutUp",
+  /* Bouncing entrances  */
+  "bounceIn",
+  "bounceInDown",
+  "bounceInLeft",
+  "bounceInRight",
+  "bounceInUp",
+  /* Bouncing exits  */
+  "bounceOut",
+  "bounceOutDown",
+  "bounceOutLeft",
+  "bounceOutRight",
+  "bounceOutUp",
+  /* Fading entrances  */
+  "fadeIn",
+  "fadeInDown",
+  "fadeInDownBig",
+  "fadeInLeft",
+  "fadeInLeftBig",
+  "fadeInRight",
+  "fadeInRightBig",
+  "fadeInUp",
+  "fadeInUpBig",
+  "fadeInTopLeft",
+  "fadeInTopRight",
+  "fadeInBottomLeft",
+  "fadeInBottomRight",
+  /* Fading exits */
+  "fadeOut",
+  "fadeOutDown",
+  "fadeOutDownBig",
+  "fadeOutLeft",
+  "fadeOutLeftBig",
+  "fadeOutRight",
+  "fadeOutRightBig",
+  "fadeOutUp",
+  "fadeOutUpBig",
+  "fadeOutTopLeft",
+  "fadeOutTopRight",
+  "fadeOutBottomRight",
+  "fadeOutBottomLeft",
+  /* Flippers */
+  "flip",
+  "flipInX",
+  "flipInY",
+  "flipOutX",
+  "flipOutY",
+  /* Lightspeed */
+  "lightSpeedInRight",
+  "lightSpeedInLeft",
+  "lightSpeedOutRight",
+  "lightSpeedOutLeft",
+  /* Rotating entrances */
+  "rotateIn",
+  "rotateInDownLeft",
+  "rotateInDownRight",
+  "rotateInUpLeft",
+  "rotateInUpRight",
+  /* Rotating exits */
+  "rotateOut",
+  "rotateOutDownLeft",
+  "rotateOutDownRight",
+  "rotateOutUpLeft",
+  "rotateOutUpRight",
+  /* Specials */
+  "hinge",
+  "jackInTheBox",
+  "rollIn",
+  "rollOut",
+  /* Zooming entrances */
+  "zoomIn",
+  "zoomInDown",
+  "zoomInLeft",
+  "zoomInRight",
+  "zoomInUp",
+  /* Zooming exits */
+  "zoomOut",
+  "zoomOutDown",
+  "zoomOutLeft",
+  "zoomOutRight",
+  "zoomOutUp",
+  /* Sliding entrances */
+  "slideInDown",
+  "slideInLeft",
+  "slideInRight",
+  "slideInUp",
+  /* Sliding exits */
+  "slideOutDown",
+  "slideOutLeft",
+  "slideOutRight",
+  "slideOutUp"
+];

+ 127 - 0
admin-ui/src/components/ReAnimateSelector/src/index.vue

@@ -0,0 +1,127 @@
+<script setup lang="ts">
+import { animates } from "./animate";
+import { ref, computed, toRef } from "vue";
+import { cloneDeep } from "@pureadmin/utils";
+
+defineOptions({
+  name: "ReAnimateSelector"
+});
+
+const props = defineProps({
+  modelValue: {
+    require: false,
+    type: String
+  }
+});
+const emit = defineEmits<{ (e: "update:modelValue", v: string) }>();
+
+const inputValue = toRef(props, "modelValue");
+const animatesList = ref(animates);
+const copyAnimatesList = cloneDeep(animatesList);
+
+const animateClass = computed(() => {
+  return [
+    "mt-1",
+    "flex",
+    "border",
+    "w-[130px]",
+    "h-[100px]",
+    "items-center",
+    "cursor-pointer",
+    "transition-all",
+    "justify-center",
+    "border-[#e5e7eb]",
+    "hover:text-primary",
+    "hover:duration-[700ms]"
+  ];
+});
+
+const animateStyle = computed(
+  () => (i: string) =>
+    inputValue.value === i
+      ? {
+          borderColor: "var(--el-color-primary)",
+          color: "var(--el-color-primary)"
+        }
+      : ""
+);
+
+function onChangeIcon(animate: string) {
+  emit("update:modelValue", animate);
+}
+
+function onClear() {
+  emit("update:modelValue", "");
+}
+
+function filterMethod(value: any) {
+  animatesList.value = copyAnimatesList.value.filter((i: string | any[]) =>
+    i.includes(value)
+  );
+}
+
+const animateMap = ref({});
+function onMouseEnter(index: string | number) {
+  animateMap.value[index] = animateMap.value[index]?.loading
+    ? Object.assign({}, animateMap.value[index], {
+        loading: false
+      })
+    : Object.assign({}, animateMap.value[index], {
+        loading: true
+      });
+}
+function onMouseleave() {
+  animateMap.value = {};
+}
+</script>
+
+<template>
+  <el-select
+    :model-value="inputValue"
+    placeholder="请选择动画"
+    clearable
+    filterable
+    :filter-method="filterMethod"
+    @clear="onClear"
+  >
+    <template #empty>
+      <div class="w-[280px]">
+        <el-scrollbar
+          noresize
+          height="212px"
+          :view-style="{ overflow: 'hidden' }"
+          class="border-t border-[#e5e7eb]"
+        >
+          <ul class="flex flex-wrap justify-around mb-1">
+            <li
+              v-for="(animate, index) in animatesList"
+              :key="index"
+              :class="animateClass"
+              :style="animateStyle(animate)"
+              @mouseenter.prevent="onMouseEnter(index)"
+              @mouseleave.prevent="onMouseleave"
+              @click="onChangeIcon(animate)"
+            >
+              <h4
+                :class="[
+                  `animate__animated animate__${
+                    animateMap[index]?.loading
+                      ? animate + ' animate__infinite'
+                      : ''
+                  } `
+                ]"
+              >
+                {{ animate }}
+              </h4>
+            </li>
+          </ul>
+          <el-empty
+            v-show="animatesList.length === 0"
+            description="暂无动画"
+            :image-size="60"
+          />
+        </el-scrollbar>
+      </div>
+    </template>
+  </el-select>
+</template>

+ 5 - 0
admin-ui/src/components/ReAuth/index.ts

@@ -0,0 +1,5 @@
+import auth from "./src/auth";
+
+const Auth = auth;
+
+export { Auth };

+ 20 - 0
admin-ui/src/components/ReAuth/src/auth.tsx

@@ -0,0 +1,20 @@
+import { defineComponent, Fragment } from "vue";
+import { hasAuth } from "@/router/utils";
+
+export default defineComponent({
+  name: "Auth",
+  props: {
+    value: {
+      type: undefined,
+      default: []
+    }
+  },
+  setup(props, { slots }) {
+    return () => {
+      if (!slots) return null;
+      return hasAuth(props.value) ? (
+        <Fragment>{slots.default?.()}</Fragment>
+      ) : null;
+    };
+  }
+});

+ 7 - 0
admin-ui/src/components/ReBarcode/index.ts

@@ -0,0 +1,7 @@
+import reBarcode from "./src/index.vue";
+import { withInstall } from "@pureadmin/utils";
+
+/** 条形码组件 */
+export const ReBarcode = withInstall(reBarcode);
+
+export default ReBarcode;

+ 42 - 0
admin-ui/src/components/ReBarcode/src/index.vue

@@ -0,0 +1,42 @@
+<script setup lang="ts">
+import JsBarcode from "jsbarcode";
+import { ref, onMounted } from "vue";
+
+defineOptions({
+  name: "ReBarcode"
+});
+
+const props = defineProps({
+  tag: {
+    type: String,
+    default: "canvas"
+  },
+  text: {
+    type: String,
+    default: null
+  },
+  // 完整配置 https://github.com/lindell/JsBarcode/wiki/Options
+  options: {
+    type: Object,
+    default() {
+      return {};
+    }
+  },
+  // type 相当于 options.format,如果 type 和 options.format 同时存在,type 值优先;
+  type: {
+    type: String,
+    default: "CODE128"
+  }
+});
+
+const wrapEl = ref(null);
+
+onMounted(() => {
+  const opt = { ...props.options, format: props.type };
+  JsBarcode(wrapEl.value, props.text, opt);
+});
+</script>
+
+<template>
+  <component :is="tag" ref="wrapEl" />
+</template>

+ 29 - 0
admin-ui/src/components/ReCol/index.ts

@@ -0,0 +1,29 @@
+import { ElCol } from "element-plus";
+import { h, defineComponent } from "vue";
+
+// 封装element-plus的el-col组件
+export default defineComponent({
+  name: "ReCol",
+  props: {
+    value: {
+      type: Number,
+      default: 24
+    }
+  },
+  render() {
+    const attrs = this.$attrs;
+    const val = this.value;
+    return h(
+      ElCol,
+      {
+        xs: val,
+        sm: val,
+        md: val,
+        lg: val,
+        xl: val,
+        ...attrs
+      },
+      { default: () => this.$slots.default() }
+    );
+  }
+});

+ 2 - 0
admin-ui/src/components/ReCountTo/README.md

@@ -0,0 +1,2 @@
+normal 普通数字动画组件  
+rebound 回弹式数字动画组件

+ 11 - 0
admin-ui/src/components/ReCountTo/index.ts

@@ -0,0 +1,11 @@
+import reNormalCountTo from "./src/normal";
+import reboundCountTo from "./src/rebound";
+import { withInstall } from "@pureadmin/utils";
+
+/** 普通数字动画组件 */
+const ReNormalCountTo = withInstall(reNormalCountTo);
+
+/** 回弹式数字动画组件 */
+const ReboundCountTo = withInstall(reboundCountTo);
+
+export { ReNormalCountTo, ReboundCountTo };

+ 179 - 0
admin-ui/src/components/ReCountTo/src/normal/index.tsx

@@ -0,0 +1,179 @@
+import {
+  defineComponent,
+  reactive,
+  computed,
+  watch,
+  onMounted,
+  unref
+} from "vue";
+import { countToProps } from "./props";
+import { isNumber } from "@pureadmin/utils";
+
+export default defineComponent({
+  name: "ReNormalCountTo",
+  props: countToProps,
+  emits: ["mounted", "callback"],
+  setup(props, { emit }) {
+    const state = reactive<{
+      localStartVal: number;
+      printVal: number | null;
+      displayValue: string;
+      paused: boolean;
+      localDuration: number | null;
+      startTime: number | null;
+      timestamp: number | null;
+      rAF: any;
+      remaining: number | null;
+      color: string;
+      fontSize: string;
+    }>({
+      localStartVal: props.startVal,
+      displayValue: formatNumber(props.startVal),
+      printVal: null,
+      paused: false,
+      localDuration: props.duration,
+      startTime: null,
+      timestamp: null,
+      remaining: null,
+      rAF: null,
+      color: null,
+      fontSize: "16px"
+    });
+
+    const getCountDown = computed(() => {
+      return props.startVal > props.endVal;
+    });
+
+    watch([() => props.startVal, () => props.endVal], () => {
+      if (props.autoplay) {
+        start();
+      }
+    });
+
+    function start() {
+      const { startVal, duration, color, fontSize } = props;
+      state.localStartVal = startVal;
+      state.startTime = null;
+      state.localDuration = duration;
+      state.paused = false;
+      state.color = color;
+      state.fontSize = fontSize;
+      state.rAF = requestAnimationFrame(count);
+    }
+
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
+    function pauseResume() {
+      if (state.paused) {
+        resume();
+        state.paused = false;
+      } else {
+        pause();
+        state.paused = true;
+      }
+    }
+
+    function pause() {
+      cancelAnimationFrame(state.rAF);
+    }
+
+    function resume() {
+      state.startTime = null;
+      state.localDuration = +(state.remaining as number);
+      state.localStartVal = +(state.printVal as number);
+      requestAnimationFrame(count);
+    }
+
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
+    function reset() {
+      state.startTime = null;
+      cancelAnimationFrame(state.rAF);
+      state.displayValue = formatNumber(props.startVal);
+    }
+
+    function count(timestamp: number) {
+      const { useEasing, easingFn, endVal } = props;
+      if (!state.startTime) state.startTime = timestamp;
+      state.timestamp = timestamp;
+      const progress = timestamp - state.startTime;
+      state.remaining = (state.localDuration as number) - progress;
+      if (useEasing) {
+        if (unref(getCountDown)) {
+          state.printVal =
+            state.localStartVal -
+            easingFn(
+              progress,
+              0,
+              state.localStartVal - endVal,
+              state.localDuration as number
+            );
+        } else {
+          state.printVal = easingFn(
+            progress,
+            state.localStartVal,
+            endVal - state.localStartVal,
+            state.localDuration as number
+          );
+        }
+      } else {
+        if (unref(getCountDown)) {
+          state.printVal =
+            state.localStartVal -
+            (state.localStartVal - endVal) *
+              (progress / (state.localDuration as number));
+        } else {
+          state.printVal =
+            state.localStartVal +
+            (endVal - state.localStartVal) *
+              (progress / (state.localDuration as number));
+        }
+      }
+      if (unref(getCountDown)) {
+        state.printVal = state.printVal < endVal ? endVal : state.printVal;
+      } else {
+        state.printVal = state.printVal > endVal ? endVal : state.printVal;
+      }
+      state.displayValue = formatNumber(state.printVal);
+      if (progress < (state.localDuration as number)) {
+        state.rAF = requestAnimationFrame(count);
+      } else {
+        emit("callback");
+      }
+    }
+
+    function formatNumber(num: number | string) {
+      const { decimals, decimal, separator, suffix, prefix } = props;
+      num = Number(num).toFixed(decimals);
+      num += "";
+      const x = num.split(".");
+      let x1 = x[0];
+      const x2 = x.length > 1 ? decimal + x[1] : "";
+      const rgx = /(\d+)(\d{3})/;
+      if (separator && !isNumber(separator)) {
+        while (rgx.test(x1)) {
+          x1 = x1.replace(rgx, "$1" + separator + "$2");
+        }
+      }
+      return prefix + x1 + x2 + suffix;
+    }
+
+    onMounted(() => {
+      if (props.autoplay) {
+        start();
+      }
+      emit("mounted");
+    });
+
+    return () => (
+      <>
+        <span
+          style={{
+            color: props.color,
+            fontSize: props.fontSize
+          }}
+        >
+          {state.displayValue}
+        </span>
+      </>
+    );
+  }
+});

+ 31 - 0
admin-ui/src/components/ReCountTo/src/normal/props.ts

@@ -0,0 +1,31 @@
+import type { PropType } from "vue";
+import propTypes from "@/utils/propTypes";
+export const countToProps = {
+  startVal: propTypes.number.def(0),
+  endVal: propTypes.number.def(2020),
+  duration: propTypes.number.def(1300),
+  autoplay: propTypes.bool.def(true),
+  decimals: {
+    type: Number as PropType<number>,
+    required: false,
+    default: 0,
+    validator(value: number) {
+      return value >= 0;
+    }
+  },
+  color: propTypes.string.def(),
+  fontSize: propTypes.string.def(),
+  decimal: propTypes.string.def("."),
+  separator: propTypes.string.def(","),
+  prefix: propTypes.string.def(""),
+  suffix: propTypes.string.def(""),
+  useEasing: propTypes.bool.def(true),
+  easingFn: {
+    type: Function as PropType<
+      (t: number, b: number, c: number, d: number) => number
+    >,
+    default(t: number, b: number, c: number, d: number) {
+      return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
+    }
+  }
+};

+ 72 - 0
admin-ui/src/components/ReCountTo/src/rebound/index.tsx

@@ -0,0 +1,72 @@
+import "./rebound.css";
+import {
+  defineComponent,
+  ref,
+  unref,
+  onBeforeMount,
+  onBeforeUnmount
+} from "vue";
+import { reboundProps } from "./props";
+
+export default defineComponent({
+  name: "ReboundCountTo",
+  props: reboundProps,
+  setup(props) {
+    const ulRef = ref();
+    const timer = ref(null);
+
+    onBeforeMount(() => {
+      const ua = navigator.userAgent.toLowerCase();
+      const testUA = regexp => regexp.test(ua);
+      const isSafari = testUA(/safari/g) && !testUA(/chrome/g);
+
+      // Safari浏览器的兼容代码
+      isSafari &&
+        (timer.value = setTimeout(() => {
+          ulRef.value.setAttribute(
+            "style",
+            `
+        animation: none;
+        transform: translateY(calc(var(--i) * -9.09%))
+      `
+          );
+        }, props.delay * 1000));
+    });
+
+    onBeforeUnmount(() => {
+      clearTimeout(unref(timer));
+    });
+
+    return () => (
+      <>
+        <div
+          class="scroll-num"
+          style={{ "--i": props.i, "--delay": props.delay }}
+        >
+          <ul ref="ulRef" style={{ fontSize: "32px" }}>
+            <li>0</li>
+            <li>1</li>
+            <li>2</li>
+            <li>3</li>
+            <li>4</li>
+            <li>5</li>
+            <li>6</li>
+            <li>7</li>
+            <li>8</li>
+            <li>9</li>
+            <li>0</li>
+          </ul>
+
+          <svg width="0" height="0">
+            <filter id="blur">
+              <feGaussianBlur
+                in="SourceGraphic"
+                stdDeviation={`0 ${props.blur}`}
+              />
+            </filter>
+          </svg>
+        </div>
+      </>
+    );
+  }
+});

+ 14 - 0
admin-ui/src/components/ReCountTo/src/rebound/props.ts

@@ -0,0 +1,14 @@
+import type { PropType } from "vue";
+import propTypes from "@/utils/propTypes";
+export const reboundProps = {
+  delay: propTypes.number.def(1),
+  blur: propTypes.number.def(2),
+  i: {
+    type: Number as PropType<number>,
+    required: false,
+    default: 0,
+    validator(value: number) {
+      return value < 10 && value >= 0 && Number.isInteger(value);
+    }
+  }
+};

+ 77 - 0
admin-ui/src/components/ReCountTo/src/rebound/rebound.css

@@ -0,0 +1,77 @@
+.scroll-num {
+  width: var(--width, 20px);
+  height: var(--height, calc(var(--width, 20px) * 1.8));
+  color: var(--color, #333);
+  font-size: var(--height, calc(var(--width, 20px) * 1.1));
+  line-height: var(--height, calc(var(--width, 20px) * 1.8));
+  text-align: center;
+  overflow: hidden;
+  animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards;
+}
+
+ul {
+  animation:
+    move 0.3s linear infinite,
+    bounce-in-down 1s calc(var(--delay) * 1s) forwards;
+}
+
+@keyframes move {
+  from {
+    transform: translateY(-90%);
+    filter: url(#blur);
+  }
+
+  to {
+    transform: translateY(1%);
+    filter: url(#blur);
+  }
+}
+
+@keyframes bounce-in-down {
+  from {
+    transform: translateY(calc(var(--i) * -9.09% - 7%));
+    filter: none;
+  }
+
+  25% {
+    transform: translateY(calc(var(--i) * -9.09% + 3%));
+  }
+
+  50% {
+    transform: translateY(calc(var(--i) * -9.09% - 1%));
+  }
+
+  70% {
+    transform: translateY(calc(var(--i) * -9.09% + 0.6%));
+  }
+
+  85% {
+    transform: translateY(calc(var(--i) * -9.09% - 0.3%));
+  }
+
+  to {
+    transform: translateY(calc(var(--i) * -9.09%));
+  }
+}
+
+@keyframes enhance-bounce-in-down {
+  25% {
+    transform: translateY(8%);
+  }
+
+  50% {
+    transform: translateY(-4%);
+  }
+
+  70% {
+    transform: translateY(2%);
+  }
+
+  85% {
+    transform: translateY(-1%);
+  }
+
+  to {
+    transform: translateY(0);
+  }
+}

+ 7 - 0
admin-ui/src/components/ReCropper/index.ts

@@ -0,0 +1,7 @@
+import reCropper from "./src";
+import { withInstall } from "@pureadmin/utils";
+
+/** 图片裁剪组件 */
+export const ReCropper = withInstall(reCropper);
+
+export default ReCropper;

+ 11 - 0
admin-ui/src/components/ReCropper/src/circled.css

@@ -0,0 +1,11 @@
+@import "cropperjs/dist/cropper.css";
+@import "tippy.js/dist/tippy.css";
+@import "tippy.js/themes/light.css";
+@import "tippy.js/animations/perspective.css";
+
+.re-circled {
+  .cropper-view-box,
+  .cropper-face {
+    border-radius: 50%;
+  }
+}

+ 439 - 0
admin-ui/src/components/ReCropper/src/index.tsx

@@ -0,0 +1,439 @@
+import "./circled.css";
+import Cropper from "cropperjs";
+import { ElUpload } from "element-plus";
+import type { CSSProperties } from "vue";
+import { useResizeObserver } from "@vueuse/core";
+import { longpress } from "@/directives/longpress";
+import { useTippy, directive as tippy } from "vue-tippy";
+import { delay, debounce, isArray, downloadByBase64 } from "@pureadmin/utils";
+import {
+  ref,
+  unref,
+  computed,
+  type PropType,
+  onMounted,
+  onUnmounted,
+  defineComponent
+} from "vue";
+import {
+  Reload,
+  Upload,
+  ArrowH,
+  ArrowV,
+  ArrowUp,
+  ArrowDown,
+  ArrowLeft,
+  ChangeIcon,
+  ArrowRight,
+  RotateLeft,
+  SearchPlus,
+  RotateRight,
+  SearchMinus,
+  DownloadIcon
+} from "./svg";
+
+type Options = Cropper.Options;
+
+const defaultOptions: Options = {
+  aspectRatio: 1,
+  zoomable: true,
+  zoomOnTouch: true,
+  zoomOnWheel: true,
+  cropBoxMovable: true,
+  cropBoxResizable: true,
+  toggleDragModeOnDblclick: true,
+  autoCrop: true,
+  background: true,
+  highlight: true,
+  center: true,
+  responsive: true,
+  restore: true,
+  checkCrossOrigin: true,
+  checkOrientation: true,
+  scalable: true,
+  modal: true,
+  guides: true,
+  movable: true,
+  rotatable: true
+};
+
+const props = {
+  src: { type: String, required: true },
+  alt: { type: String },
+  circled: { type: Boolean, default: false },
+  realTimePreview: { type: Boolean, default: true },
+  height: { type: [String, Number], default: "360px" },
+  crossorigin: {
+    type: String as PropType<"" | "anonymous" | "use-credentials" | undefined>,
+    default: undefined
+  },
+  imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
+  options: { type: Object as PropType<Options>, default: () => ({}) }
+};
+
+export default defineComponent({
+  name: "ReCropper",
+  props,
+  setup(props, { attrs, emit }) {
+    const tippyElRef = ref<ElRef<HTMLImageElement>>();
+    const imgElRef = ref<ElRef<HTMLImageElement>>();
+    const cropper = ref<Nullable<Cropper>>();
+    const isReady = ref(false);
+    const imgBase64 = ref();
+    const inCircled = ref(props.circled);
+    const inSrc = ref(props.src);
+    let scaleX = 1;
+    let scaleY = 1;
+
+    const debounceRealTimeCroppered = debounce(realTimeCroppered, 80);
+
+    const getImageStyle = computed((): CSSProperties => {
+      return {
+        height: props.height,
+        maxWidth: "100%",
+        ...props.imageStyle
+      };
+    });
+
+    const getClass = computed(() => {
+      return [
+        attrs.class,
+        {
+          ["re-circled"]: inCircled.value
+        }
+      ];
+    });
+
+    const iconClass = computed(() => {
+      return [
+        "p-[6px]",
+        "h-[30px]",
+        "w-[30px]",
+        "outline-none",
+        "rounded-[4px]",
+        "cursor-pointer",
+        "hover:bg-[rgba(0,0,0,0.06)]"
+      ];
+    });
+
+    const getWrapperStyle = computed((): CSSProperties => {
+      return { height: `${props.height}`.replace(/px/, "") + "px" };
+    });
+
+    onMounted(init);
+
+    onUnmounted(() => {
+      cropper.value?.destroy();
+    });
+
+    useResizeObserver(tippyElRef, () => {
+      handCropper("reset");
+    });
+
+    async function init() {
+      const imgEl = unref(imgElRef);
+      if (!imgEl) return;
+      cropper.value = new Cropper(imgEl, {
+        ...defaultOptions,
+        ready: () => {
+          isReady.value = true;
+          realTimeCroppered();
+          delay(400).then(() => emit("readied", cropper.value));
+        },
+        crop() {
+          debounceRealTimeCroppered();
+        },
+        zoom() {
+          debounceRealTimeCroppered();
+        },
+        cropmove() {
+          debounceRealTimeCroppered();
+        },
+        ...props.options
+      });
+    }
+
+    function realTimeCroppered() {
+      props.realTimePreview && croppered();
+    }
+
+    function croppered() {
+      if (!cropper.value) return;
+      const canvas = inCircled.value
+        ? getRoundedCanvas()
+        : cropper.value.getCroppedCanvas();
+      // https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/toBlob
+      canvas.toBlob(blob => {
+        if (!blob) return;
+        const fileReader: FileReader = new FileReader();
+        fileReader.readAsDataURL(blob);
+        fileReader.onloadend = e => {
+          if (!e.target?.result || !blob) return;
+          imgBase64.value = e.target.result;
+          emit("cropper", {
+            base64: e.target.result,
+            blob,
+            info: { size: blob.size, ...cropper.value.getData() }
+          });
+        };
+        fileReader.onerror = () => {
+          emit("error");
+        };
+      });
+    }
+
+    function getRoundedCanvas() {
+      const sourceCanvas = cropper.value!.getCroppedCanvas();
+      const canvas = document.createElement("canvas");
+      const context = canvas.getContext("2d")!;
+      const width = sourceCanvas.width;
+      const height = sourceCanvas.height;
+      canvas.width = width;
+      canvas.height = height;
+      context.imageSmoothingEnabled = true;
+      context.drawImage(sourceCanvas, 0, 0, width, height);
+      context.globalCompositeOperation = "destination-in";
+      context.beginPath();
+      context.arc(
+        width / 2,
+        height / 2,
+        Math.min(width, height) / 2,
+        0,
+        2 * Math.PI,
+        true
+      );
+      context.fill();
+      return canvas;
+    }
+
+    function handCropper(event: string, arg?: number | Array<number>) {
+      if (event === "scaleX") {
+        scaleX = arg = scaleX === -1 ? 1 : -1;
+      }
+      if (event === "scaleY") {
+        scaleY = arg = scaleY === -1 ? 1 : -1;
+      }
+      arg && isArray(arg)
+        ? cropper.value?.[event]?.(...arg)
+        : cropper.value?.[event]?.(arg);
+    }
+
+    function beforeUpload(file) {
+      const reader = new FileReader();
+      reader.readAsDataURL(file);
+      inSrc.value = "";
+      reader.onload = e => {
+        inSrc.value = e.target?.result as string;
+      };
+      reader.onloadend = () => {
+        init();
+      };
+      return false;
+    }
+
+    const menuContent = defineComponent({
+      directives: {
+        tippy,
+        longpress
+      },
+      setup() {
+        return () => (
+          <div class="flex flex-wrap w-[60px] justify-between">
+            <ElUpload
+              accept="image/*"
+              show-file-list={false}
+              before-upload={beforeUpload}
+            >
+              <Upload
+                class={iconClass.value}
+                v-tippy={{
+                  content: "上传",
+                  placement: "left-start"
+                }}
+              />
+            </ElUpload>
+            <DownloadIcon
+              class={iconClass.value}
+              v-tippy={{
+                content: "下载",
+                placement: "right-start"
+              }}
+              onClick={() => downloadByBase64(imgBase64.value, "cropping.png")}
+            />
+            <ChangeIcon
+              class={iconClass.value}
+              v-tippy={{
+                content: "圆形、矩形裁剪",
+                placement: "left-start"
+              }}
+              onClick={() => {
+                inCircled.value = !inCircled.value;
+                realTimeCroppered();
+              }}
+            />
+            <Reload
+              class={iconClass.value}
+              v-tippy={{
+                content: "重置",
+                placement: "right-start"
+              }}
+              onClick={() => handCropper("reset")}
+            />
+            <ArrowUp
+              class={iconClass.value}
+              v-tippy={{
+                content: "上移(可长按)",
+                placement: "left-start"
+              }}
+              v-longpress={[() => handCropper("move", [0, -10]), "0:100"]}
+            />
+            <ArrowDown
+              class={iconClass.value}
+              v-tippy={{
+                content: "下移(可长按)",
+                placement: "right-start"
+              }}
+              v-longpress={[() => handCropper("move", [0, 10]), "0:100"]}
+            />
+            <ArrowLeft
+              class={iconClass.value}
+              v-tippy={{
+                content: "左移(可长按)",
+                placement: "left-start"
+              }}
+              v-longpress={[() => handCropper("move", [-10, 0]), "0:100"]}
+            />
+            <ArrowRight
+              class={iconClass.value}
+              v-tippy={{
+                content: "右移(可长按)",
+                placement: "right-start"
+              }}
+              v-longpress={[() => handCropper("move", [10, 0]), "0:100"]}
+            />
+            <ArrowH
+              class={iconClass.value}
+              v-tippy={{
+                content: "水平翻转",
+                placement: "left-start"
+              }}
+              onClick={() => handCropper("scaleX", -1)}
+            />
+            <ArrowV
+              class={iconClass.value}
+              v-tippy={{
+                content: "垂直翻转",
+                placement: "right-start"
+              }}
+              onClick={() => handCropper("scaleY", -1)}
+            />
+            <RotateLeft
+              class={iconClass.value}
+              v-tippy={{
+                content: "逆时针旋转",
+                placement: "left-start"
+              }}
+              onClick={() => handCropper("rotate", -45)}
+            />
+            <RotateRight
+              class={iconClass.value}
+              v-tippy={{
+                content: "顺时针旋转",
+                placement: "right-start"
+              }}
+              onClick={() => handCropper("rotate", 45)}
+            />
+            <SearchPlus
+              class={iconClass.value}
+              v-tippy={{
+                content: "放大(可长按)",
+                placement: "left-start"
+              }}
+              v-longpress={[() => handCropper("zoom", 0.1), "0:100"]}
+            />
+            <SearchMinus
+              class={iconClass.value}
+              v-tippy={{
+                content: "缩小(可长按)",
+                placement: "right-start"
+              }}
+              v-longpress={[() => handCropper("zoom", -0.1), "0:100"]}
+            />
+          </div>
+        );
+      }
+    });
+
+    function onContextmenu(event) {
+      event.preventDefault();
+
+      const { show, setProps } = useTippy(tippyElRef, {
+        content: menuContent,
+        arrow: false,
+        theme: "light",
+        trigger: "manual",
+        interactive: true,
+        appendTo: "parent",
+        // hideOnClick: false,
+        animation: "perspective",
+        placement: "bottom-end"
+      });
+
+      setProps({
+        getReferenceClientRect: () => ({
+          width: 0,
+          height: 0,
+          top: event.clientY,
+          bottom: event.clientY,
+          left: event.clientX,
+          right: event.clientX
+        })
+      });
+
+      show();
+    }
+
+    return {
+      inSrc,
+      props,
+      imgElRef,
+      tippyElRef,
+      getClass,
+      getWrapperStyle,
+      getImageStyle,
+      isReady,
+      croppered,
+      onContextmenu
+    };
+  },
+
+  render() {
+    const {
+      inSrc,
+      isReady,
+      getClass,
+      getImageStyle,
+      onContextmenu,
+      getWrapperStyle
+    } = this;
+    const { alt, crossorigin } = this.props;
+
+    return inSrc ? (
+      <div
+        ref="tippyElRef"
+        class={getClass}
+        style={getWrapperStyle}
+        onContextmenu={event => onContextmenu(event)}
+      >
+        <img
+          v-show={isReady}
+          ref="imgElRef"
+          style={getImageStyle}
+          src={inSrc}
+          alt={alt}
+          crossorigin={crossorigin}
+        />
+      </div>
+    ) : null;
+  }
+});

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/arrow-down.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0 0 48.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2z"/></svg>

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/arrow-h.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m296.992 216.992-272 272L3.008 512l21.984 23.008 272 272 46.016-46.016L126.016 544h772L680.992 760.992l46.016 46.016 272-272L1020.992 512l-21.984-23.008-272-272-46.048 46.048L898.016 480h-772l216.96-216.992z"/></svg>

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/arrow-left.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M872 474H286.9l350.2-304c5.6-4.9 2.2-14-5.2-14h-88.5c-3.9 0-7.6 1.4-10.5 3.9L155 487.8a31.96 31.96 0 0 0 0 48.3L535.1 866c1.5 1.3 3.3 2 5.2 2h91.5c7.4 0 10.8-9.2 5.2-14L286.9 550H872c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"/></svg>

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/arrow-right.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M869 487.8 491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-.7 5.2-2L869 536.2a32.07 32.07 0 0 0 0-48.4z"/></svg>

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/arrow-up.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M868 545.5 536.1 163a31.96 31.96 0 0 0-48.3 0L156 545.5a7.97 7.97 0 0 0 6 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z"/></svg>

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/arrow-v.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m512 67.008-23.008 21.984-256 256 46.048 46.048L480 190.016v644L279.008 632.96l-46.048 46.08 256 256 23.008 21.984 23.008-21.984 256-256-46.016-46.016L544 834.016v-644l200.992 200.96 46.016-45.984-256-256z"/></svg>

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/change.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="M956.8 988.8H585.6c-16 0-25.6-9.6-25.6-28.8V576c0-16 9.6-28.8 25.6-28.8h371.2c16 0 25.6 9.6 25.6 28.8v384c0 16-9.6 28.8-25.6 28.8zM608 937.6h326.4V598.4H608v339.2zm-121.6 44.8C262.4 982.4 144 848 144 595.2c0-19.2 9.6-28.8 25.6-28.8s25.6 12.8 25.6 28.8c0 220.8 96 326.4 288 326.4 16 0 25.6 12.8 25.6 28.8s-6.4 32-22.4 32z"/><path d="M262.4 694.4c-6.4 0-9.6-3.2-16-6.4L160 601.6c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8-3.2 3.2-6.4 6.4-12.8 6.4z"/><path d="M86.4 694.4c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0 9.6 9.6 9.6 22.4 0 28.8L99.2 688c-3.2 3.2-6.4 6.4-12.8 6.4zm790.4-249.6c-16 0-28.8-12.8-28.8-32 0-224-99.2-336-300.8-336-16 0-28.8-12.8-28.8-32s9.6-32 28.8-32c233.6 0 355.2 137.6 355.2 396.8 0 22.4-9.6 35.2-25.6 35.2z"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4l-86.4-86.4c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8 0 3.2-6.4 6.4-12.8 6.4z"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0s9.6 22.4 0 28.8l-86.4 86.4c-3.2 3.2-6.4 6.4-12.8 6.4zM288 524.8C156.8 524.8 48 416 48 278.4S156.8 35.2 288 35.2 528 144 528 281.6 419.2 524.8 288 524.8zm-3.2-432c-99.2 0-179.2 83.2-179.2 185.6S185.6 464 284.8 464 464 380.8 464 278.4 384 92.8 284.8 92.8z"/></svg>

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/download.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M505.7 661a8 8 0 0 0 12.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"/></svg>

+ 31 - 0
admin-ui/src/components/ReCropper/src/svg/index.ts

@@ -0,0 +1,31 @@
+import Reload from "./reload.svg?component";
+import Upload from "./upload.svg?component";
+import ArrowH from "./arrow-h.svg?component";
+import ArrowV from "./arrow-v.svg?component";
+import ArrowUp from "./arrow-up.svg?component";
+import ChangeIcon from "./change.svg?component";
+import ArrowDown from "./arrow-down.svg?component";
+import ArrowLeft from "./arrow-left.svg?component";
+import DownloadIcon from "./download.svg?component";
+import ArrowRight from "./arrow-right.svg?component";
+import RotateLeft from "./rotate-left.svg?component";
+import SearchPlus from "./search-plus.svg?component";
+import RotateRight from "./rotate-right.svg?component";
+import SearchMinus from "./search-minus.svg?component";
+
+export {
+  Reload,
+  Upload,
+  ArrowH,
+  ArrowV,
+  ArrowUp,
+  ArrowDown,
+  ArrowLeft,
+  ChangeIcon,
+  ArrowRight,
+  RotateLeft,
+  SearchPlus,
+  RotateRight,
+  SearchMinus,
+  DownloadIcon
+};

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/reload.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 0 1 755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 0 0 3 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8zm756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 0 1 512.1 856a342.24 342.24 0 0 1-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 0 0-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 0 0-8-8.2z"/></svg>

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/rotate-left.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H188V494h440v326z"/><path fill="currentColor" d="M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7.4 12.6-6.1v-63.9c12.9.1 25.9.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8 11 40.7 14 82.7 8.9 124.8-.7 5.4-1.4 10.8-2.4 16.1h74.9c14.8-103.6-11.3-213-81-302.3z"/></svg>

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/rotate-right.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-.4-12.6 6.1l-.2 64c-118.6.5-235.8 53.4-314.6 154.2-69.6 89.2-95.7 198.6-81.1 302.4h74.9c-.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8z"/><path fill="currentColor" d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H396V494h440v326z"/></svg>

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/search-minus.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"/></svg>

+ 1 - 0
admin-ui/src/components/ReCropper/src/svg/search-plus.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"/></svg>

部分文件因文件數量過多而無法顯示