xiongzhu vor 1 Jahr
Commit
ed8c27f029
56 geänderte Dateien mit 5023 neuen und 0 gelöschten Zeilen
  1. 3 0
      .env.development
  2. 3 0
      .env.production
  3. 17 0
      .eslintrc.cjs
  4. 28 0
      .gitignore
  5. 8 0
      .prettierrc.json
  6. 35 0
      README.md
  7. 14 0
      index.html
  8. 9 0
      jsconfig.json
  9. 45 0
      package.json
  10. 6 0
      postcss.config.js
  11. 0 0
      public/app-info-parser.min.js
  12. BIN
      public/favicon.ico
  13. 84 0
      src/App.vue
  14. BIN
      src/assets/1.jpg
  15. BIN
      src/assets/2.jpg
  16. BIN
      src/assets/2.webp
  17. 19 0
      src/assets/logo.svg
  18. 84 0
      src/components/ChangePwd.vue
  19. 41 0
      src/components/DarkSwitch.vue
  20. 97 0
      src/components/EditDialog.vue
  21. 52 0
      src/components/EnumSelect.vue
  22. 22 0
      src/components/ListView.vue
  23. 44 0
      src/components/LogoSvg.vue
  24. 112 0
      src/components/MultiUpload.vue
  25. 117 0
      src/components/PagingTable.vue
  26. 19 0
      src/components/SideMenu.vue
  27. 173 0
      src/components/SingleUpload.vue
  28. 30 0
      src/components/SubMenus.vue
  29. 11 0
      src/components/UserAvatar.vue
  30. 39 0
      src/enums/index.js
  31. 18 0
      src/main.js
  32. 114 0
      src/plugins/http.js
  33. 139 0
      src/router/index.js
  34. 12 0
      src/stores/counter.js
  35. 11 0
      src/stores/user.js
  36. 14 0
      src/styles/main.less
  37. 9 0
      src/utils/editDialog.js
  38. 17 0
      src/utils/formatter.js
  39. 13 0
      src/utils/imageSize.js
  40. 15 0
      src/views/AboutView.vue
  41. 118 0
      src/views/ChannelView.vue
  42. 140 0
      src/views/DealerView.vue
  43. 121 0
      src/views/DeviceView.vue
  44. 5 0
      src/views/HomeView.vue
  45. 58 0
      src/views/LoginView.vue
  46. 205 0
      src/views/MainView.vue
  47. 4 0
      src/views/NotFoundView.vue
  48. 161 0
      src/views/PhoneListView.vue
  49. 77 0
      src/views/RcsNumberView.vue
  50. 75 0
      src/views/SysConfigView.vue
  51. 217 0
      src/views/TaskView.vue
  52. 73 0
      src/views/UserView.vue
  53. 9 0
      tailwind.config.js
  54. 38 0
      vite.config.js
  55. 15 0
      volar.config.js
  56. 2233 0
      yarn.lock

+ 3 - 0
.env.development

@@ -0,0 +1,3 @@
+VITE_BASE_URL=/
+VITE_API_BASE_URL=http://localhost:3000/api
+VITE_WS_URL=ws://localhost:3000

+ 3 - 0
.env.production

@@ -0,0 +1,3 @@
+VITE_BASE_URL=/admin/
+VITE_API_BASE_URL=/api
+VITE_WS_URL=/

+ 17 - 0
.eslintrc.cjs

@@ -0,0 +1,17 @@
+/* eslint-env node */
+require('@rushstack/eslint-patch/modern-module-resolution')
+
+module.exports = {
+    root: true,
+    extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-prettier/skip-formatting'],
+    parserOptions: {
+        ecmaVersion: 'latest'
+    },
+    rules: {
+        'no-unused-vars': 'off'
+    },
+    globals: {
+        process: true,
+        AppInfoParser: true
+    }
+}

+ 28 - 0
.gitignore

@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 8 - 0
.prettierrc.json

@@ -0,0 +1,8 @@
+{
+    "$schema": "https://json.schemastore.org/prettierrc",
+    "semi": false,
+    "tabWidth": 4,
+    "singleQuote": true,
+    "printWidth": 120,
+    "trailingComma": "none"
+}

+ 35 - 0
README.md

@@ -0,0 +1,35 @@
+# chat-admin
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Compile and Minify for Production
+
+```sh
+npm run build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+npm run lint
+```

+ 14 - 0
index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>RCS</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+    <script src="/app-info-parser.min.js"></script>
+  </body>
+</html>

+ 9 - 0
jsconfig.json

@@ -0,0 +1,9 @@
+{
+    "compilerOptions": {
+        "paths": {
+            "@/*": ["./src/*"]
+        },
+        "types": ["element-plus/global"]
+    },
+    "include": ["./src/**/*"]
+}

+ 45 - 0
package.json

@@ -0,0 +1,45 @@
+{
+  "name": "chat-admin",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "@vicons/tabler": "^0.12.0",
+    "@vueuse/core": "^10.1.0",
+    "axios": "^1.3.6",
+    "date-fns": "^2.29.3",
+    "element-plus": "^2.3.3",
+    "file-saver": "^2.0.5",
+    "moment": "^2.30.1",
+    "moment-timezone": "^0.5.45",
+    "pinia": "^2.0.32",
+    "qs": "^6.11.1",
+    "resolve-url": "^0.2.1",
+    "socket.io-client": "^4.7.2",
+    "vue": "^3.2.47",
+    "vue-json-viewer": "3",
+    "vue-router": "^4.1.6"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "^1.2.0",
+    "@vitejs/plugin-vue": "^4.0.0",
+    "@volar-plugins/prettier": "^2.0.0",
+    "@vue/eslint-config-prettier": "^7.1.0",
+    "autoprefixer": "^10.4.14",
+    "eslint": "^8.34.0",
+    "eslint-plugin-vue": "^9.9.0",
+    "less": "^4.1.3",
+    "postcss": "^8.4.23",
+    "prettier": "^2.8.4",
+    "tailwindcss": "^3.3.1",
+    "unplugin-auto-import": "^0.15.3",
+    "unplugin-vue-components": "^0.24.1",
+    "vite": "^4.1.4"
+  }
+}

+ 6 - 0
postcss.config.js

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

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
public/app-info-parser.min.js


BIN
public/favicon.ico


+ 84 - 0
src/App.vue

@@ -0,0 +1,84 @@
+<script setup>
+import { RouterView } from 'vue-router'
+import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
+import { useWindowSize } from '@vueuse/core'
+import { computed, provide } from 'vue'
+const { width } = useWindowSize()
+const size = computed(() => {
+    return width.value > 768 ? 'small' : 'medium'
+})
+const isMobile = computed(() => {
+    return width.value <= 768
+})
+provide('isMobile', isMobile)
+</script>
+
+<template>
+    <ElConfigProvider :locale="zhCn">
+        <RouterView />
+    </ElConfigProvider>
+</template>
+
+<style scoped>
+header {
+    line-height: 1.5;
+    max-height: 100vh;
+}
+
+.logo {
+    display: block;
+    margin: 0 auto 2rem;
+}
+
+nav {
+    width: 100%;
+    font-size: 12px;
+    text-align: center;
+    margin-top: 2rem;
+}
+
+nav a.router-link-exact-active {
+    color: var(--color-text);
+}
+
+nav a.router-link-exact-active:hover {
+    background-color: transparent;
+}
+
+nav a {
+    display: inline-block;
+    padding: 0 1rem;
+    border-left: 1px solid var(--color-border);
+}
+
+nav a:first-of-type {
+    border: 0;
+}
+
+@media (min-width: 1024px) {
+    header {
+        display: flex;
+        place-items: center;
+        padding-right: calc(var(--section-gap) / 2);
+    }
+
+    .logo {
+        margin: 0 2rem 0 0;
+    }
+
+    header .wrapper {
+        display: flex;
+        place-items: flex-start;
+        flex-wrap: wrap;
+    }
+
+    nav {
+        text-align: left;
+        margin-left: -1rem;
+        font-size: 1rem;
+
+        padding: 1rem 0;
+        margin-top: 1rem;
+    }
+}
+</style>

BIN
src/assets/1.jpg


BIN
src/assets/2.jpg


BIN
src/assets/2.webp


+ 19 - 0
src/assets/logo.svg

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>logo</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="首页" transform="translate(-16.000000, -99.000000)" fill="#FFFFFF">
+            <g id="编组" transform="translate(0.000000, 88.000000)">
+                <g id="LOGO备份" transform="translate(16.000000, 11.000000)">
+                    <g id="编组-83" transform="translate(2.000000, 2.000000)">
+                        <path d="M12.2873658,0 C5.50124107,0 0,5.50124107 0,12.2873658 C0,19.0734906 5.50124107,24.5747317 12.2873658,24.5747317 C16.654991,24.5747317 20.6756592,22.2687245 22.8877214,18.5048014 L23,18.2749865 L21.3784725,17.3841251 L21.2791651,17.5569806 C19.4011277,20.7524439 15.9924659,22.7076469 12.2873658,22.7076469 C6.53240351,22.7076469 1.86708476,18.0423282 1.86708476,12.2873658 C1.86708476,6.53240351 6.53240351,1.86708476 12.2873658,1.86708476 C15.9068605,1.86708476 19.2475092,3.73215926 21.1531561,6.80897852 L21.269837,6.99736911 L22.8877214,6.05928988 L22.740454,5.82587583 C20.4946423,2.19983339 16.553942,0 12.2873658,0 Z" id="路径" fill-rule="nonzero"></path>
+                        <path d="M12.2088942,4.1512942 C7.63585277,4.1512942 3.92867021,7.85847676 3.92867021,12.4315182 C3.92867021,17.0045596 7.63585277,20.7117422 12.2088942,20.7117422 C15.1519195,20.7117422 17.8615887,19.1576867 19.3520559,16.6215898 L19.4843929,16.3467403 L17.795713,15.3900755 L17.6564225,15.6250639 C16.5190013,17.5604372 14.4540159,18.7449589 12.2088942,18.7449589 C8.72207716,18.7449589 5.89545346,15.9183352 5.89545346,12.4315182 C5.89545346,8.94470115 8.72207716,6.11807745 12.2088942,6.11807745 C14.4021708,6.11807745 16.4259009,7.24797072 17.5807579,9.11257974 L17.795713,9.45259814 L19.4843929,8.48632353 L19.2528143,8.07698139 C17.7396268,5.63381927 15.0838062,4.1512942 12.2088942,4.1512942 Z" id="路径" fill-rule="nonzero"></path>
+                        <polygon id="矩形备份-141" transform="translate(19.953421, 17.593844) rotate(28.000000) translate(-19.953421, -17.593844) " points="17.1026675 16.6653561 22.8041752 16.759414 22.8041752 18.5223317 17.1026675 18.4282738"></polygon>
+                        <path d="M12.4411076,7.73990028 C9.84999861,7.73990028 7.74948972,9.84040917 7.74948972,12.4315182 C7.74948972,15.0226272 9.84999861,17.1231361 12.4411076,17.1231361 C14.1083576,17.1231361 15.6439322,16.2425024 16.488198,14.8059462 L16.6004766,14.6148991 L14.9365502,13.5556816 L14.7925646,13.8094203 C14.3013443,14.6452537 13.4104532,15.1563529 12.4411076,15.1563529 C10.936223,15.1563529 9.71627297,13.9364028 9.71627297,12.4315182 C9.71627297,10.9266336 10.936223,9.70668353 12.4411076,9.70668353 C13.3881059,9.70668353 14.261128,10.1941769 14.7599098,10.9994999 L14.8765906,11.1878905 L16.5343848,10.212967 L16.4319661,9.96390153 C15.5748544,8.58002615 14.0697422,7.73990028 12.4411076,7.73990028 Z" id="路径" fill-rule="nonzero"></path>
+                        <polygon id="矩形" transform="translate(16.651762, 9.124785) rotate(-31.000000) translate(-16.651762, -9.124785) " points="14.0762884 8.21676072 19.2272361 8.2698908 19.2272361 10.0328085 14.0762884 9.97967843"></polygon>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 84 - 0
src/components/ChangePwd.vue

@@ -0,0 +1,84 @@
+<template>
+    <ElDialog
+        class="!w-10/12 max-w-lg"
+        title="修改密码"
+        :model-value="modelValue"
+        @update:model-value="setVisible"
+        @open="onOpen"
+    >
+        <ElForm :model="model" :rules="rules" ref="formEl" label-position="top">
+            <ElFormItem label="新密码" prop="password">
+                <ElInput v-model="model.password" show-password></ElInput>
+            </ElFormItem>
+            <ElFormItem label="确认密码" prop="repeat">
+                <ElInput v-model="model.repeat" show-password></ElInput>
+            </ElFormItem>
+        </ElForm>
+        <template #footer>
+            <ElButton @click="setVisible(false)">取消</ElButton>
+            <ElButton type="primary" @click="submit" :loading="loading">确定</ElButton>
+        </template>
+    </ElDialog>
+</template>
+<script setup>
+import { ref } from 'vue'
+import { http } from '@/plugins/http'
+import { ElMessage } from 'element-plus'
+const props = defineProps({
+    modelValue: {
+        type: Boolean,
+        default: false
+    }
+})
+const emit = defineEmits(['update:modelValue'])
+const model = ref({
+    password: '',
+    repeat: ''
+})
+const formEl = ref(null)
+const rules = {
+    password: [
+        { required: true, message: '请输入密码', trigger: 'blur' },
+        { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
+    ],
+    repeat: [
+        { required: true, message: '请再次输入密码', trigger: 'blur' },
+        { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
+        {
+            validator: (rule, value, callback) => {
+                if (value !== model.value.password) {
+                    callback(new Error('两次输入密码不一致'))
+                } else {
+                    callback()
+                }
+            },
+            trigger: 'blur'
+        }
+    ]
+}
+function setVisible(val) {
+    emit('update:modelValue', val)
+}
+const loading = ref(false)
+function submit() {
+    formEl.value.validate(async (valid) => {
+        if (valid) {
+            loading.value = true
+            try {
+                await http.post('/users/updatePassword', { password: model.value.password })
+                setVisible(false)
+                ElMessage.success('修改成功')
+            } catch (e) {
+                ElMessage.error(e.message)
+            } finally {
+                loading.value = false
+            }
+        }
+    })
+}
+function onOpen() {
+    if (formEl.value) {
+        formEl.value.clearValidate()
+    }
+}
+</script>

+ 41 - 0
src/components/DarkSwitch.vue

@@ -0,0 +1,41 @@
+<template>
+    <div class="dark-mode-switch cursor-pointer bg-neutral-300 dark:bg-neutral-700" :class="{ active: isDark }" @click="toggleDark()" title="切换黑暗模式">
+        <div class="switch-indicator absolute rounded-lg bg-white dark:bg-neutral-800 !transition-all">
+            <component :is="isDark ? Moon : Sun" />
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { Sun, Moon } from '@vicons/tabler'
+import { useDark, useToggle } from '@vueuse/core'
+const isDark = useDark({
+    storageKey: 'dark-mode-admin'
+})
+const toggleDark = useToggle(isDark)
+</script>
+
+<style lang="less" scoped>
+.dark-mode-switch {
+    width: 40px;
+    height: 20px;
+    border-radius: 10px;
+    padding: 2px;
+    box-sizing: border-box;
+    position: relative;
+    .switch-indicator {
+        width: 16px;
+        height: 16px;
+        padding: 1px;
+        left: 2px;
+        top: 0;
+        bottom: 0;
+        margin: auto;
+    }
+    &.active {
+        .switch-indicator {
+            left: 22px;
+        }
+    }
+}
+</style>

+ 97 - 0
src/components/EditDialog.vue

@@ -0,0 +1,97 @@
+<template>
+    <ElDialog
+        v-model="show"
+        :title="title"
+        :class="{ '!w-10/12': !width, 'max-w-3xl': !width }"
+        :close-on-click-modal="!saving"
+        :close-on-press-escape="!saving"
+        :width="width"
+    >
+        <ElForm :model="model" :rules="rules" ref="formEl" label-position="right" :label-width="labelWidth">
+            <slot></slot>
+        </ElForm>
+        <div class="text-right">
+            <ElButton @click="show = false" :disabled="saving">取消</ElButton>
+            <ElButton type="primary" @click="onSave" :loading="saving">保存</ElButton>
+        </div>
+    </ElDialog>
+</template>
+<script setup>
+import { computed, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+const props = defineProps({
+    modelValue: {
+        type: Boolean,
+        default: false
+    },
+    title: {
+        type: String,
+        default: '编辑'
+    },
+    model: {
+        type: Object,
+        default: () => ({})
+    },
+    rules: {
+        type: Object,
+        default: () => ({})
+    },
+    onSubmit: {
+        type: Function,
+        default: () => {}
+    },
+    labelWidth: {
+        type: String,
+        default: '80px'
+    },
+    width: {
+        type: String
+    }
+})
+const emit = defineEmits(['update:modelValue', 'success', 'error'])
+const show = computed({
+    get() {
+        return props.modelValue
+    },
+    set(modelValue) {
+        emit('update:modelValue', modelValue)
+    }
+})
+
+const formEl = ref(null)
+const saving = ref(false)
+async function onSave() {
+    try {
+        try {
+            await formEl.value.validate()
+        } catch (e) {
+            return
+        }
+        saving.value = true
+        await props.onSubmit()
+        saving.value = false
+        show.value = false
+        emit('success')
+    } catch (e) {
+        saving.value = false
+        if (e.message) {
+            if (typeof e.message === 'string') {
+                ElMessage.error(e.message)
+            } else {
+                ElMessage.error(JSON.stringify(e.message))
+            }
+        } else {
+            ElMessage.error('保存失败')
+        }
+        emit('error', e)
+    }
+}
+watch(
+    () => props.modelValue,
+    (modelValue) => {
+        if (modelValue && formEl.value) {
+            formEl.value.clearValidate()
+        }
+    }
+)
+</script>

+ 52 - 0
src/components/EnumSelect.vue

@@ -0,0 +1,52 @@
+<template>
+    <ElSelect
+        :modelValue="modelValue"
+        @change="change"
+        :placeholder="placeholder"
+        :clearable="clearable"
+        @clear="clear"
+        :multiple="multiple"
+        :multipleLimit="multipleLimit"
+    >
+        <ElOption v-for="item in options" :key="item.value" :value="item.value" :label="item.label" />
+    </ElSelect>
+</template>
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+    enum: {
+        type: Object,
+        required: true
+    },
+    modelValue: {},
+    placeholder: {
+        type: String,
+        default: '请选择'
+    },
+    clearable: {
+        type: Boolean,
+        default: true
+    },
+    multiple: {
+        type: Boolean,
+        default: false
+    },
+    multipleLimit: {}
+})
+const emit = defineEmits(['update:modelValue'])
+const options = computed(() => {
+    return Object.keys(props.enum).map((key) => {
+        return {
+            label: props.enum[key],
+            value: key
+        }
+    })
+})
+function change(value) {
+    emit('update:modelValue', value)
+}
+function clear() {
+    emit('update:modelValue', null)
+}
+</script>

+ 22 - 0
src/components/ListView.vue

@@ -0,0 +1,22 @@
+<template>
+    <div class="filter" ref="filterEl">
+        <slot name="filter"></slot>
+    </div>
+    <slot></slot>
+</template>
+<script setup>
+import { ref, provide, computed } from 'vue'
+import { useElementBounding, useWindowSize } from '@vueuse/core'
+const filterEl = ref(null)
+const { height: filterHeight } = useElementBounding(filterEl)
+const { height: windowHeight } = useWindowSize()
+const tableHeight = computed(() => windowHeight.value - 145 - filterHeight.value)
+provide('tableHeight', tableHeight)
+</script>
+<style lang="less" scoped>
+.filter {
+    :deep(> *) {
+        margin-bottom: 10px;
+    }
+}
+</style>

+ 44 - 0
src/components/LogoSvg.vue

@@ -0,0 +1,44 @@
+<template>
+    <svg
+        width="28px"
+        height="28px"
+        viewBox="0 0 28 28"
+        version="1.1"
+        xmlns="http://www.w3.org/2000/svg"
+        xmlns:xlink="http://www.w3.org/1999/xlink"
+    >
+        <title>logo</title>
+        <g id="页面-1" stroke="none" stroke-width="1">
+            <g id="首页" transform="translate(-16.000000, -99.000000)">
+                <g id="编组" transform="translate(0.000000, 88.000000)">
+                    <g id="LOGO备份" transform="translate(16.000000, 11.000000)">
+                        <g id="编组-83" transform="translate(2.000000, 2.000000)">
+                            <path
+                                d="M12.2873658,0 C5.50124107,0 0,5.50124107 0,12.2873658 C0,19.0734906 5.50124107,24.5747317 12.2873658,24.5747317 C16.654991,24.5747317 20.6756592,22.2687245 22.8877214,18.5048014 L23,18.2749865 L21.3784725,17.3841251 L21.2791651,17.5569806 C19.4011277,20.7524439 15.9924659,22.7076469 12.2873658,22.7076469 C6.53240351,22.7076469 1.86708476,18.0423282 1.86708476,12.2873658 C1.86708476,6.53240351 6.53240351,1.86708476 12.2873658,1.86708476 C15.9068605,1.86708476 19.2475092,3.73215926 21.1531561,6.80897852 L21.269837,6.99736911 L22.8877214,6.05928988 L22.740454,5.82587583 C20.4946423,2.19983339 16.553942,0 12.2873658,0 Z"
+                                id="路径"
+                            ></path>
+                            <path
+                                d="M12.2088942,4.1512942 C7.63585277,4.1512942 3.92867021,7.85847676 3.92867021,12.4315182 C3.92867021,17.0045596 7.63585277,20.7117422 12.2088942,20.7117422 C15.1519195,20.7117422 17.8615887,19.1576867 19.3520559,16.6215898 L19.4843929,16.3467403 L17.795713,15.3900755 L17.6564225,15.6250639 C16.5190013,17.5604372 14.4540159,18.7449589 12.2088942,18.7449589 C8.72207716,18.7449589 5.89545346,15.9183352 5.89545346,12.4315182 C5.89545346,8.94470115 8.72207716,6.11807745 12.2088942,6.11807745 C14.4021708,6.11807745 16.4259009,7.24797072 17.5807579,9.11257974 L17.795713,9.45259814 L19.4843929,8.48632353 L19.2528143,8.07698139 C17.7396268,5.63381927 15.0838062,4.1512942 12.2088942,4.1512942 Z"
+                                id="路径"
+                            ></path>
+                            <polygon
+                                id="矩形备份-141"
+                                transform="translate(19.953421, 17.593844) rotate(28.000000) translate(-19.953421, -17.593844) "
+                                points="17.1026675 16.6653561 22.8041752 16.759414 22.8041752 18.5223317 17.1026675 18.4282738"
+                            ></polygon>
+                            <path
+                                d="M12.4411076,7.73990028 C9.84999861,7.73990028 7.74948972,9.84040917 7.74948972,12.4315182 C7.74948972,15.0226272 9.84999861,17.1231361 12.4411076,17.1231361 C14.1083576,17.1231361 15.6439322,16.2425024 16.488198,14.8059462 L16.6004766,14.6148991 L14.9365502,13.5556816 L14.7925646,13.8094203 C14.3013443,14.6452537 13.4104532,15.1563529 12.4411076,15.1563529 C10.936223,15.1563529 9.71627297,13.9364028 9.71627297,12.4315182 C9.71627297,10.9266336 10.936223,9.70668353 12.4411076,9.70668353 C13.3881059,9.70668353 14.261128,10.1941769 14.7599098,10.9994999 L14.8765906,11.1878905 L16.5343848,10.212967 L16.4319661,9.96390153 C15.5748544,8.58002615 14.0697422,7.73990028 12.4411076,7.73990028 Z"
+                                id="路径"
+                            ></path>
+                            <polygon
+                                id="矩形"
+                                transform="translate(16.651762, 9.124785) rotate(-31.000000) translate(-16.651762, -9.124785) "
+                                points="14.0762884 8.21676072 19.2272361 8.2698908 19.2272361 10.0328085 14.0762884 9.97967843"
+                            ></polygon>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </svg>
+</template>

+ 112 - 0
src/components/MultiUpload.vue

@@ -0,0 +1,112 @@
+<template>
+    <section>
+        <el-upload
+            list-type="picture-card"
+            accept="image/*"
+            :action="uploadUrl"
+            :on-preview="handlePictureCardPreview"
+            :on-success="handleSuccess"
+            :on-remove="handleRemove"
+            v-model:file-list="fileList"
+            @before-upload="beforeUpload"
+            multiple
+        >
+            <el-icon>
+                <Plus />
+            </el-icon>
+            <template #tip>
+                <div class="el-upload__tip">
+                    <slot></slot>
+                </div>
+            </template>
+        </el-upload>
+        <ElImageViewer v-if="showPreview" :url-list="[previewUrl]" teleported @close="showPreview = false" />
+    </section>
+</template>
+<script setup>
+import resolveUrl from 'resolve-url'
+import { ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Plus } from '@vicons/tabler'
+import { useImageSize } from '@/utils/imageSize'
+
+const props = defineProps({
+    modelValue: Array,
+    maxWidth: {
+        type: Number,
+        default: -1
+    },
+    maxHeight: {
+        type: Number,
+        default: -1
+    },
+    maxSize: {
+        type: Number,
+        default: 1024 * 1024
+    }
+})
+const emit = defineEmits(['update:modelValue'])
+const uploadUrl = resolveUrl(import.meta.env.VITE_API_BASE_URL, '/api/file/upload')
+const fileList = ref([])
+const loading = ref(false)
+const showPreview = ref(false)
+const previewUrl = ref(null)
+watch(
+    () => props.modelValue,
+    (val) => {
+        if (val && val.join() === fileList.value.map((i) => i.realUrl).join()) {
+            return
+        }
+        updateFileList(val)
+    }
+)
+updateFileList(props.modelValue || [])
+function updateFileList(list) {
+    fileList.value = list.map((i) => {
+        return {
+            url: i,
+            realUrl: i
+        }
+    })
+}
+
+async function beforeUpload(file) {
+    if (!/^image/.test(file.type)) {
+        this.$message.error('只能上传图片格式!')
+        return false
+    }
+    if (this.maxSize > 0 && file.size > this.maxSize) {
+        this.$message.error('上传图片大小不能超过 ' + this.maxSize / 1024 + 'KB!')
+        return false
+    }
+    const { width, height } = await useImageSize(URL.createObjectURL(file))
+    if (props.maxWidth > 0 && width > props.maxWidth) {
+        this.$message.error('上传图片宽度不能超过 ' + props.maxWidth + 'px!')
+        return false
+    } else if (props.maxHeight > 0 && height > props.maxHeight) {
+        this.$message.error('上传图片高度不能超过 ' + props.maxHeight + 'px!')
+        return false
+    } else {
+        loading.value = true
+        return true
+    }
+}
+function handlePictureCardPreview(file) {
+    previewUrl.value = file.url || file.realUrl
+    showPreview.value = true
+}
+function handleSuccess(res, file, fileList) {
+    file.realUrl = res.url
+    emit(
+        'update:modelValue',
+        fileList.map((i) => i.realUrl)
+    )
+}
+function handleRemove(file, fileList) {
+    emit(
+        'update:modelValue',
+        fileList.map((i) => i.realUrl)
+    )
+}
+</script>
+<style lang="less" scoped></style>

+ 117 - 0
src/components/PagingTable.vue

@@ -0,0 +1,117 @@
+<template>
+    <div class="filter" ref="filterEl">
+        <slot name="filter"></slot>
+    </div>
+    <ElConfigProvider :size="isMobile ? '' : 'small'">
+        <ElTable :data="tableData" :height="height || tableHeight" stripe v-loading="loading">
+            <slot></slot>
+        </ElTable>
+    </ElConfigProvider>
+    <div class="mt-4 flex justify-center">
+        <ElPagination
+            ref="paginEl"
+            :layout="isMobile ? 'total, pager' : 'total, sizes, prev, pager, next, jumper'"
+            v-model:page-size="pageConfig.pageSize"
+            v-model:current-page="page"
+            :total="total"
+            :small="!isMobile"
+        />
+    </div>
+</template>
+<script setup>
+import { ref, onMounted, computed, watch, inject } from 'vue'
+import { http } from '@/plugins/http'
+import { ElMessage } from 'element-plus'
+import { useStorage, useElementBounding, useWindowSize } from '@vueuse/core'
+
+const props = defineProps({
+    url: {
+        type: String,
+        required: true
+    },
+    where: {
+        type: Object,
+        default: () => ({})
+    },
+    order: {
+        type: Object,
+        default: () => ({ createdAt: 'DESC' })
+    },
+    height: {}
+})
+const search = computed(() => {
+    const where = { ...(props.where || {}) }
+    Object.keys(where).forEach((key) => {
+        if (where[key] === null) {
+            delete where[key]
+        }
+    })
+    return {
+        where: props.where,
+        order: props.order
+    }
+})
+
+const filterEl = ref(null)
+const paginEl = ref(null)
+const { height: filterHeight } = useElementBounding(filterEl)
+const { height: paginHeight } = useElementBounding(paginEl)
+const { height: windowHeight } = useWindowSize()
+const tableHeight = computed(() => windowHeight.value - 120 - filterHeight.value - paginHeight.value)
+const isMobile = inject('isMobile')
+
+const tableData = ref([])
+const page = ref(1)
+const pageConfig = useStorage('pageConfig', {
+    pageSize: 20
+})
+
+const total = ref(0)
+const loading = ref(false)
+async function getData() {
+    try {
+        loading.value = true
+        const res = await http.post(props.url, {
+            page: {
+                page: page.value,
+                limit: pageConfig.value.pageSize
+            },
+            search: search.value
+        })
+        loading.value = false
+        tableData.value = res.items
+        total.value = res.meta.totalItems
+    } catch (e) {
+        loading.value = false
+        ElMessage.error(e.message)
+    }
+}
+onMounted(() => {
+    getData()
+})
+watch(search, () => {
+    console.log('search changed')
+    if (page.value !== 1) {
+        page.value = 1
+    } else {
+        getData()
+    }
+})
+watch([page, pageConfig], () => {
+    console.log('page changed')
+    getData()
+})
+defineExpose({
+    refresh() {
+        getData()
+    }
+})
+</script>
+<style lang="less" scoped>
+.filter {
+    :deep(> *) {
+        margin-bottom: 15px;
+        margin-right: 15px;
+    }
+}
+</style>

+ 19 - 0
src/components/SideMenu.vue

@@ -0,0 +1,19 @@
+<template>
+    <ElMenu class="side-menu !border-0 bg-slate-100 dark:bg-zinc-800" :default-active="defaultActive" :router="true" unique-opened>
+        <SubMenus :menus="menus" />
+    </ElMenu>
+</template>
+<script setup>
+import SubMenus from './SubMenus.vue'
+
+const props = defineProps({
+    defaultActive: {
+        type: String,
+        default: 'home'
+    },
+    menus: {
+        type: Array,
+        default: () => []
+    }
+})
+</script>

+ 173 - 0
src/components/SingleUpload.vue

@@ -0,0 +1,173 @@
+<template>
+    <el-upload
+        v-model:file-list="fileList"
+        class="single-upload"
+        :action="uploadUrl"
+        :show-file-list="false"
+        :on-success="onSuccess"
+        :before-upload="beforeUpload"
+        accept="image/*"
+        @on-change="onChange"
+    >
+        <template #trigger>
+            <div
+                class="w-40 h-40 bg-neutral-100 dark:bg-neutral-900 rounded-md cursor-pointer border border-dashed border-gray-500 hover:border-blue-500 flex justify-center items-center overflow-hidden relative"
+                v-loading="loading"
+            >
+                <img v-if="imageUrl" :src="imageUrl" class="w-full h-full object-cover" />
+                <Plus v-if="!imageUrl && !loading" class="w-10 h-10 text-gray-400"> </Plus>
+                <div v-if="imageUrl" class="absolute bottom-4 left-0 right-0 flex items-center justify-center">
+                    <Eye
+                        class="w-5 h-5 text-neutral-100 drop-shadow-md cursor-pointer"
+                        @click.stop="showPreview = true"
+                    />
+                    <Trash class="w-5 h-5 text-neutral-100 drop-shadow-md cursor-pointer ml-4" @click.stop="clear" />
+                </div>
+            </div>
+        </template>
+        <template #tip>
+            <div class="el-upload__tip">
+                <slot></slot>
+            </div>
+        </template>
+    </el-upload>
+    <ElImageViewer v-if="showPreview" :url-list="[imageUrl]" teleported @close="showPreview = false" />
+</template>
+<script setup>
+import { ref, unref, watch } from 'vue'
+import resolveUrl from 'resolve-url'
+import { Plus, Upload, Trash, Eye } from '@vicons/tabler'
+import { useImageSize } from '@/utils/imageSize'
+const props = defineProps({
+    modelValue: String,
+    usePrefix: {
+        type: Boolean,
+        default: true
+    },
+    url: {
+        type: String
+    },
+    maxWidth: {
+        type: Number,
+        default: -1
+    },
+    maxHeight: {
+        type: Number,
+        default: -1
+    },
+    maxSize: {
+        type: Number,
+        default: 1024 * 1024
+    }
+})
+const emit = defineEmits(['update:modelValue'])
+const uploadUrl = resolveUrl(import.meta.env.VITE_API_BASE_URL, '/api/file/upload')
+const loading = ref(false)
+const fileList = ref([])
+const imageUrl = ref(null)
+const showPreview = ref(false)
+
+if (props.modelValue) {
+    imageUrl.value = unref(props.modelValue)
+    fileList.value = [
+        {
+            url: unref(props.modelValue)
+        }
+    ]
+}
+watch(
+    () => props.modelValue,
+    (val) => {
+        imageUrl.value = val
+        fileList.value = [
+            {
+                url: val
+            }
+        ]
+    }
+)
+function onSuccess(res, file) {
+    loading.value = false
+    imageUrl.value = URL.createObjectURL(file.raw)
+    emit('update:modelValue', res.url)
+}
+function onError(err, file, fileList) {
+    loading.value = false
+}
+async function beforeUpload(file) {
+    if (!/^image/.test(file.type)) {
+        this.$message.error('只能上传图片格式!')
+        return false
+    }
+    if (this.maxSize > 0 && file.size > this.maxSize) {
+        this.$message.error('上传图片大小不能超过 ' + this.maxSize / 1024 + 'KB!')
+        return false
+    }
+    const { width, height } = await useImageSize(URL.createObjectURL(file))
+    if (props.maxWidth > 0 && width > props.maxWidth) {
+        this.$message.error('上传图片宽度不能超过 ' + props.maxWidth + 'px!')
+        return false
+    } else if (props.maxHeight > 0 && height > props.maxHeight) {
+        this.$message.error('上传图片高度不能超过 ' + props.maxHeight + 'px!')
+        return false
+    } else {
+        loading.value = true
+        return true
+    }
+}
+
+function onChange(e) {
+    console.log(e)
+}
+function clear() {
+    emit('update:modelValue', null)
+}
+</script>
+<style lang="less" scoped>
+.single-uploader-icon {
+    color: var(--el-text-color-secondary);
+    border: 1px dashed var(--el-border-color);
+    border-radius: var(--el-border-radius-base);
+    background-color: var(--el-bg-color);
+}
+
+.upload-image {
+    height: 178px;
+    display: block;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+
+    &:hover {
+        border-color: #409eff;
+    }
+}
+
+.wrapper {
+    position: relative;
+    font-size: 12px;
+}
+
+.single-upload .el-upload {
+    position: relative;
+}
+
+.loading {
+    position: absolute;
+    top: 1px;
+    bottom: 1px;
+    left: 1px;
+    right: 1px;
+    margin: auto;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: rgba(255, 255, 255, 0.6);
+    color: #333;
+    font-size: 24px;
+    border-radius: 7px;
+    overflow: hidden;
+}
+</style>

+ 30 - 0
src/components/SubMenus.vue

@@ -0,0 +1,30 @@
+<template>
+    <template v-for="item in menus" :key="item.name">
+        <ElSubMenu v-if="item.children" :index="item.name" class="bg-slate-100 dark:bg-zinc-800">
+            <template #title>
+                <div class="w-5 h-5 mr-2" v-if="item.icon">
+                    <component :is="item.icon"></component>
+                </div>
+                {{ item.title }}
+            </template>
+            <SubMenus :menus="item.children"></SubMenus>
+        </ElSubMenu>
+        <ElMenuItem v-else :index="item.name" class="bg-slate-100 dark:bg-zinc-800">
+            <template #title>
+                <div class="w-5 h-5 mr-2" v-if="item.icon">
+                    <component :is="item.icon"></component>
+                </div>
+                {{ item.title }}
+            </template>
+        </ElMenuItem>
+    </template>
+</template>
+<script setup>
+import { computed } from 'vue'
+const props = defineProps({
+    menus: {
+        type: Array,
+        default: () => []
+    }
+})
+</script>

+ 11 - 0
src/components/UserAvatar.vue

@@ -0,0 +1,11 @@
+<template>
+    <img v-if="user.avatar" />
+    <User v-else class="w-8 h-8 p-1.5 rounded-3xl bg-slate-400 dark:bg-gray-600 text-white" />
+</template>
+<script setup>
+import { storeToRefs } from 'pinia'
+import { useUserStore } from '@/stores/user'
+import { User } from '@vicons/tabler'
+
+const { user } = storeToRefs(useUserStore())
+</script>

+ 39 - 0
src/enums/index.js

@@ -0,0 +1,39 @@
+export const WithdrawStatus = {
+    PENDING: '待处理',
+    SUCCESS: '完成',
+    REJECTED: '拒绝'
+}
+
+export const MemberOrderStatus = {
+    NOT_PAID: '未支付',
+    FINISH: '已完成',
+    CANCEL: '已取消'
+}
+
+export const PayMethod = {
+    WECHAT: '微信',
+    ALIPAY: '支付宝'
+}
+
+export const MemberType = {
+    TRIAL: '试用会员',
+    PAID: '付费会员'
+}
+
+export const UserRole = {
+    user: '普通用户',
+    admin: '管理员',
+    api: 'API用户'
+}
+
+export const GameStatus = {
+    created: '未初始化',
+    initialized: '已初始化',
+    running: '运行中',
+    finished: '已结束'
+}
+export const StreamPlatform = {
+    twitch: 'Twitch',
+    bilibili: 'B站',
+    douyu: '斗鱼'
+}

+ 18 - 0
src/main.js

@@ -0,0 +1,18 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+
+import App from './App.vue'
+import router from './router'
+import ElementPlus from 'element-plus'
+
+import './styles/main.less'
+import 'element-plus/dist/index.css'
+import 'element-plus/theme-chalk/dark/css-vars.css'
+
+const app = createApp(App)
+
+app.use(createPinia())
+app.use(router)
+app.use(ElementPlus)
+
+app.mount('#app')

+ 114 - 0
src/plugins/http.js

@@ -0,0 +1,114 @@
+import axios from 'axios'
+import qs from 'qs'
+import { useStorage } from '@vueuse/core'
+
+const baseURL = import.meta.env.VITE_API_BASE_URL
+const axiosInstance = axios.create({ baseURL })
+
+const token = useStorage('admin-token', '', localStorage)
+if (token.value) {
+    axiosInstance.defaults.headers.common['Authorization'] = 'Bearer ' + token.value
+}
+
+axiosInstance.interceptors.response.use(
+    function (response) {
+        return response
+    },
+    function (error) {
+        let errorData = {}
+        if (!error.response) {
+            errorData = {
+                message: 'Network Error'
+            }
+        } else {
+            errorData = error.response.data
+        }
+        if (typeof errorData != 'object') {
+            errorData = {
+                message: 'Request failed: ' + error.response.status
+            }
+        }
+        return Promise.reject(errorData)
+    }
+)
+
+const http = {
+    token,
+    baseURL,
+    resolve(path) {
+        let base = baseURL
+        if (!baseURL.startsWith('http')) {
+            base = new URL(baseURL, window.location.origin).href
+        }
+        return new URL(path, base).href
+    },
+    setToken(_token) {
+        token.value = _token
+        if (_token) {
+            axiosInstance.defaults.headers.common['Authorization'] = 'Bearer ' + _token
+        } else {
+            axiosInstance.defaults.headers.common['Authorization'] = null
+        }
+    },
+    get(url, params) {
+        return new Promise((resolve, reject) => {
+            axiosInstance
+                .get(url, { params, withCredentials: true })
+                .then((res) => {
+                    resolve(res.data)
+                })
+                .catch((e) => {
+                    reject(e)
+                })
+        })
+    },
+    post(url, body, options) {
+        options = options || {}
+        body = body || {}
+        return new Promise((resolve, reject) => {
+            axiosInstance
+                .post(url, body, { ...options, withCredentials: true })
+                .then((res) => {
+                    resolve(res.data)
+                })
+                .catch((e) => {
+                    reject(e)
+                })
+        })
+    },
+    put(url, body, options) {
+        options = options || {}
+        body = body || {}
+        return new Promise((resolve, reject) => {
+            axiosInstance
+                .put(url, body, { ...options, withCredentials: true })
+                .then((res) => {
+                    resolve(res.data)
+                })
+                .catch((e) => {
+                    reject(e)
+                })
+        })
+    },
+    delete(url, params) {
+        return new Promise((resolve, reject) => {
+            axiosInstance
+                .delete(url, { params, withCredentials: true })
+                .then((res) => {
+                    resolve(res.data)
+                })
+                .catch((e) => {
+                    reject(e)
+                })
+        })
+    }
+}
+
+export default {
+    install: (app) => {
+        app.config.globalProperties.$http = http
+        app.provide('http', app.config.globalProperties.$http)
+    },
+    http
+}
+export { http, axiosInstance }

+ 139 - 0
src/router/index.js

@@ -0,0 +1,139 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import { useUserStore } from '@/stores/user'
+import { http } from '@/plugins/http'
+
+const router = createRouter({
+    history: createWebHistory(import.meta.env.BASE_URL),
+    routes: [
+        {
+            path: '/login',
+            name: 'login',
+            component: () => import('../views/LoginView.vue')
+        },
+        {
+            path: '/',
+            redirect: '/home',
+            component: () => import('../views/MainView.vue'),
+            children: [
+                {
+                    path: '404',
+                    name: '404',
+                    component: () => import('../views/NotFoundView.vue'),
+                    meta: {
+                        title: '页面不存在'
+                    }
+                },
+                {
+                    path: 'home',
+                    name: 'home',
+                    component: () => import('../views/HomeView.vue'),
+                    meta: {
+                        title: '主页'
+                    }
+                },
+                {
+                    path: '/user',
+                    name: 'user',
+                    component: () => import('../views/UserView.vue'),
+                    meta: {
+                        title: '用户列表'
+                    }
+                },
+                {
+                    path: '/dealer',
+                    name: 'dealer',
+                    component: () => import('../views/DealerView.vue'),
+                    meta: {
+                        title: '经销商列表'
+                    }
+                },
+                {
+                    path: 'sysConfig',
+                    name: 'sysConfig',
+                    component: () => import('../views/SysConfigView.vue'),
+                    meta: {
+                        title: '系统设置'
+                    }
+                },
+                {
+                    path: '/phoneList',
+                    name: 'phoneList',
+                    component: () => import('../views/PhoneListView.vue'),
+                    meta: {
+                        title: '料子列表'
+                    }
+                },
+                {
+                    path: '/task',
+                    name: 'task',
+                    component: () => import('../views/TaskView.vue'),
+                    meta: {
+                        title: '任务列表'
+                    }
+                },
+                {
+                    path: '/device',
+                    name: 'device',
+                    component: () => import('../views/DeviceView.vue'),
+                    meta: {
+                        title: '设备列表'
+                    }
+                },
+                {
+                    path: '/channel',
+                    name: 'channel',
+                    component: () => import('../views/ChannelView.vue'),
+                    meta: {
+                        title: '渠道列表'
+                    }
+                },
+                {
+                    path: '/rcsNumber',
+                    name: 'rcsNumber',
+                    component: () => import('../views/RcsNumberView.vue'),
+                    meta: {
+                        title: 'RCS号码'
+                    }
+                }
+            ]
+        }
+    ]
+})
+
+router.beforeEach(async (to, from, next) => {
+    //console.log(to);
+    if (/^\/http/.test(to.path)) {
+        let url = to.path.replace('/', '')
+        let params = []
+        if (to.query) {
+            for (let key in to.query) {
+                // eslint-disable-next-line no-prototype-builtins
+                if (to.query.hasOwnProperty(key)) {
+                    params.push(`${key}=${to.query[key]}`)
+                }
+            }
+        }
+        if (params.length > 0) {
+            url += `?${params.join('&')}`
+        }
+        window.open(url)
+        return
+    }
+    const { user, setUser } = useUserStore()
+    if (!user && to.name !== 'login') {
+        try {
+            const res = await http.get('/admin/users/get')
+            setUser(res)
+            next()
+        } catch (err) {
+            http.setToken(null)
+            next({ name: 'login' })
+        }
+    } else if (!to.matched.length) {
+        next('/404')
+    } else {
+        next()
+    }
+})
+
+export default router

+ 12 - 0
src/stores/counter.js

@@ -0,0 +1,12 @@
+import { ref, computed } from 'vue'
+import { defineStore } from 'pinia'
+
+export const useCounterStore = defineStore('counter', () => {
+  const count = ref(0)
+  const doubleCount = computed(() => count.value * 2)
+  function increment() {
+    count.value++
+  }
+
+  return { count, doubleCount, increment }
+})

+ 11 - 0
src/stores/user.js

@@ -0,0 +1,11 @@
+import { ref, computed } from 'vue'
+import { defineStore } from 'pinia'
+
+export const useUserStore = defineStore('user', () => {
+    const user = ref(null)
+    function setUser(newUser) {
+        user.value = newUser
+    }
+
+    return { user, setUser }
+})

+ 14 - 0
src/styles/main.less

@@ -0,0 +1,14 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+html,
+body,
+#app {
+    @apply h-full;
+    @apply transition-all;
+}
+
+@font-face {
+    font-family: 'sh';
+    src: url(https://cdn.raex.vip/font/2023-03-24-10-09-25HtghnVXP.ttf);
+}

+ 9 - 0
src/utils/editDialog.js

@@ -0,0 +1,9 @@
+import { defineProps, ref } from 'vue'
+export function setupEditDialog(model) {
+    const showEditDialog = ref(false)
+    function onEdit(row) {
+        model.value = row ? { ...row } : {}
+        showEditDialog.value = true
+    }
+    return { showEditDialog, onEdit }
+}

+ 17 - 0
src/utils/formatter.js

@@ -0,0 +1,17 @@
+import { format } from 'date-fns'
+export function useTimeFormatter(pattern = 'yyyy-MM-dd HH:mm') {
+    return function (row, column, value, index) {
+        if(!value) return ''
+        try {
+            return format(new Date(value), pattern)
+        } catch (error) {
+            return ''
+        }
+    }
+}
+
+export function useEnumFormatter(enumObj) {
+    return function (row, column, value, index) {
+        return enumObj[value]
+    }
+}

+ 13 - 0
src/utils/imageSize.js

@@ -0,0 +1,13 @@
+export function useImageSize(src) {
+    return new Promise((resolve, reject) => {
+        const img = new Image()
+        img.onload = () => {
+            resolve({
+                width: img.width,
+                height: img.height
+            })
+        }
+        img.onerror = reject
+        img.src = src
+    })
+}

+ 15 - 0
src/views/AboutView.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="about">
+    <h1>This is an about page</h1>
+  </div>
+</template>
+
+<style>
+@media (min-width: 1024px) {
+  .about {
+    min-height: 100vh;
+    display: flex;
+    align-items: center;
+  }
+}
+</style>

+ 118 - 0
src/views/ChannelView.vue

@@ -0,0 +1,118 @@
+<template>
+    <PagingTable url="/channel" :where="where" ref="table">
+        <template #filter>
+            <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
+        </template>
+        <ElTableColumn prop="id" label="#" width="80" />
+        <ElTableColumn prop="mcc" label="MCC" />
+        <ElTableColumn prop="mnc" label="MNC" />
+        <ElTableColumn prop="country" label="国家" />
+        <ElTableColumn prop="operator" label="运营商" />
+        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn label="操作" align="center" width="200">
+            <template #default="{ row }">
+                <ElButton type="danger" size="small" @click="del(row)">删除</ElButton>
+            </template>
+        </ElTableColumn>
+    </PagingTable>
+    <EditDialog v-model="showEditDialog" :model="model" :rules="rules" :on-submit="submit" @success="table.refresh()">
+        <ElFormItem prop="mcc" label="MCC">
+            <ElInput v-model="model.mcc" placeholder="请输入MCC" />
+        </ElFormItem>
+        <ElFormItem prop="mnc" label="MNC">
+            <ElInput v-model="model.mnc" placeholder="请输入MNC" />
+        </ElFormItem>
+        <ElFormItem prop="country" label="国家">
+            <ElInput v-model="model.country" placeholder="请输入国家" />
+        </ElFormItem>
+        <ElFormItem prop="operator" label="运营商">
+            <ElInput v-model="model.operator" placeholder="请输入运营商" />
+        </ElFormItem>
+        <ElFormItem prop="remark" label="备注">
+            <ElInput v-model="model.remark" placeholder="请输入备注" />
+        </ElFormItem>
+    </EditDialog>
+</template>
+<script setup>
+import { ref } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter } from '@/utils/formatter'
+import { Plus } from '@vicons/tabler'
+import EditDialog from '@/components/EditDialog.vue'
+import { setupEditDialog } from '@/utils/editDialog'
+import EnumSelect from '@/components/EnumSelect.vue'
+import { UserRole } from '@/enums'
+import { http } from '@/plugins/http'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useClipboard } from '@vueuse/core'
+
+const where = ref({})
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const rules = {
+    mcc: [{ required: true, message: '请输入MCC', trigger: 'blur' }],
+    mnc: [{ required: true, message: '请输入MNC', trigger: 'blur' }],
+    country: [{ required: true, message: '请输入国家码', trigger: 'blur' }],
+    operator: [{ required: true, message: '请输入运营商', trigger: 'blur' }]
+}
+const { showEditDialog, onEdit } = setupEditDialog(model)
+
+async function submit() {
+    await http.put('/channel', model.value)
+    ElMessage.success('保存成功')
+}
+
+function del(row) {
+    ElMessageBox.confirm('确定删除吗?', '提示', {
+        type: 'warning'
+    }).then(() => {
+        http.delete(`/phone-list/${row.id}`).then(() => {
+            ElMessage.success('删除成功')
+            table.value.refresh()
+        })
+    })
+}
+
+const selectedRow = ref(null)
+const showDetailDialog = ref(false)
+const phoneTable = ref(null)
+
+const phoneModel = ref({})
+const phoneRules = {
+    number: [{ required: true, message: '请输入号码', trigger: 'blur' }]
+}
+const { showEditDialog: showPhoneEditDialog, onEdit: onPhoneEdit } = setupEditDialog(phoneModel)
+
+async function submitPhone() {
+    phoneModel.value.listId = selectedRow.value.id
+    await http.put('/phone-list/phone', phoneModel.value)
+    ElMessage.success('保存成功')
+}
+
+function delPhone(row) {
+    ElMessageBox.confirm('确定删除吗?', '提示', {
+        type: 'warning'
+    }).then(() => {
+        http.delete(`/phone-list/phone/${row.id}`).then(() => {
+            ElMessage.success('删除成功')
+            phoneTable.value.refresh()
+        })
+    })
+}
+
+function importList() {
+    const input = document.createElement('input')
+    input.type = 'file'
+    input.accept = '.txt'
+    input.onchange = async () => {
+        const file = input.files[0]
+        const formData = new FormData()
+        formData.append('file', file)
+        await http.post(`/phone-list/${selectedRow.value.id}/import`, formData)
+        ElMessage.success('导入成功')
+        phoneTable.value.refresh()
+    }
+    input.click()
+}
+</script>

+ 140 - 0
src/views/DealerView.vue

@@ -0,0 +1,140 @@
+<template>
+    <PagingTable url="/admin/users" :where="where" ref="table">
+        <template #filter>
+            <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
+        </template>
+        <ElTableColumn prop="id" label="#" width="80" />
+        <ElTableColumn prop="username" label="经销商名" min-width="120" />
+        <ElTableColumn prop="name" label="昵称" min-width="120" />
+        <ElTableColumn prop="phone" label="手机" min-width="120" />
+        <ElTableColumn prop="createdAt" label="注册时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn prop="invitor" label="上级" />
+        <ElTableColumn prop="balance" label="余额" />
+        <ElTableColumn prop="rate" label="费率" />
+        <ElTableColumn prop="send" label="已发送" />
+        <ElTableColumn label="操作" align="center" width="300">
+            <template #default="{ row }">
+                <ElButton plain @click="recharge(row.id)">余额充值</ElButton>
+                <ElButton plain @click="rate(row.id)">修改费率</ElButton>
+                <ElButton type="primary" size="small" @click="detail(row)">充值记录</ElButton>
+            </template>
+        </ElTableColumn>
+    </PagingTable>
+    <EditDialog v-model="showEditDialog" :model="model" :rules="rules" :on-submit="submit" @success="table.refresh()">
+        <ElFormItem prop="username" label="经销商名">
+            <ElInput v-model="model.username" placeholder="请输入经销商名" />
+        </ElFormItem>
+        <ElFormItem prop="name" label="昵称">
+            <ElInput v-model="model.name" placeholder="请输入昵称" />
+        </ElFormItem>
+        <ElFormItem prop="phone" label="手机">
+            <ElInput v-model="model.phone" placeholder="请输入手机" />
+        </ElFormItem>
+        <ElFormItem prop="password" label="密码">
+            <ElInput v-model="model.password" placeholder="请输入密码" />
+        </ElFormItem>
+    </EditDialog>
+    <ElDialog v-model="showDetailDialog" title="详情" width="500px">
+        <PagingTable url="/balance/records/" :where="{ userId: (selectedRow || {}).id }" :order="{createDate:'DESC'}"
+                     ref="balanceTable" height="50vh">
+            <ElTableColumn prop="id" label="#" width="80" />
+            <ElTableColumn prop="amount" label="金额" min-width="120" />
+            <ElTableColumn prop="createDate" label="时间" min-width="120" />
+        </PagingTable>
+    </ElDialog>
+</template>
+<script setup>
+import { ref } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter } from '@/utils/formatter'
+import { Plus } from '@vicons/tabler'
+import EditDialog from '@/components/EditDialog.vue'
+import { setupEditDialog } from '@/utils/editDialog'
+import EnumSelect from '@/components/EnumSelect.vue'
+import { UserRole } from '@/enums'
+import { http } from '@/plugins/http'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useClipboard } from '@vueuse/core'
+import { useUserStore } from '@/stores/user'
+
+const where = ref({ roles: 'user' })
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const { user } = useUserStore()
+const rules = {
+    username: [{ required: true, message: '请输入经销商名', trigger: 'blur' }],
+    name: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
+    phone: [{ required: true, message: '请输入手机', trigger: 'blur' }],
+    password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+    roles: [{ required: true, message: '请选择角色', trigger: 'blur' }]
+}
+const { showEditDialog, onEdit } = setupEditDialog(model)
+
+async function submit() {
+    // 经销商
+    model.value.roles = ['user']
+    // 上级
+    model.value.invitor = user.id
+    await http.put('/admin/users', model.value)
+    ElMessage.success('保存成功')
+}
+
+const recharge = async (userId) => {
+    ElMessageBox.prompt('请输入充值金额', '余额充值', {
+        confirmButtonText: '确认',
+        cancelButtonText: '取消',
+        inputPattern: /^\d+(\.\d{1,2})?$/,
+        inputErrorMessage: '金额不能超出俩位小数!'
+    }).then(async ({ value }) => {
+        console.log(userId)
+        console.log(value)
+        const url = '/balance/recharge/' + userId + '/' + value
+        await http.get(url)
+        table.value.refresh()
+        ElMessage({
+            type: 'success',
+            message: `充值成功`
+        })
+    }).catch(() => {
+        ElMessage({
+            type: 'info',
+            message: '取消充值'
+        })
+    })
+}
+
+const rate = async (userId) => {
+    ElMessageBox.prompt('请输入最新费率', '修改费率', {
+        confirmButtonText: '确认',
+        cancelButtonText: '取消',
+        inputPattern: /^\d+(\.\d{1,2})?$/,
+        inputErrorMessage: '金额不能超出俩位小数!'
+    }).then(async ({ value }) => {
+        console.log(userId)
+        console.log(value)
+        const url = '/balance/updateRate/' + userId + '/' + value
+        await http.get(url)
+        table.value.refresh()
+        ElMessage({
+            type: 'success',
+            message: `费率修改成功`
+        })
+    }).catch(() => {
+        ElMessage({
+            type: 'info',
+            message: '取消费率修改'
+        })
+    })
+}
+
+const selectedRow = ref(null)
+const showDetailDialog = ref(false)
+const balanceTable = ref(null)
+
+function detail(row) {
+    selectedRow.value = row
+    showDetailDialog.value = true
+}
+
+</script>

+ 121 - 0
src/views/DeviceView.vue

@@ -0,0 +1,121 @@
+<template>
+    <PagingTable url="/device" :where="where" ref="table">
+        <template #filter>
+            <ElButton :icon="Refresh" @click="table.refresh()"></ElButton>
+            <ElSelect v-model="online" placeholder="在线状态" clearable @change="updateWhereAndRefresh">
+                <ElOption v-for="status in onlineStatus" :key="status.id" :label="status.label" :value="status.id" />
+            </ElSelect>
+            <ElSelect v-model="canSend" placeholder="发送状态" clearable @change="updateWhereAndRefresh">
+                <ElOption v-for="status in canSendStatus" :key="status.id" :label="status.label" :value="status.id" />
+            </ElSelect>
+            <ElSelect v-model="busy" placeholder="忙碌状态" clearable @change="updateWhereAndRefresh">
+                <ElOption v-for="status in busyStatus" :key="status.id" :label="status.label" :value="status.id" />
+            </ElSelect>
+        </template>
+        <ElTableColumn prop="id" label="#" width="200" />
+        <ElTableColumn prop="model" label="型号" />
+        <ElTableColumn prop="name" label="编号" />
+        <ElTableColumn prop="online" label="在线" align="center">
+            <template #default="{ row }">
+                <ElTag type="success" v-if="row.online">在线</ElTag>
+                <ElTag type="info" v-else>离线</ElTag>
+            </template>
+        </ElTableColumn>
+        <ElTableColumn prop="canSend" label="可以发送" align="center">
+            <template #default="{ row }">
+                <ElTag type="success" v-if="row.canSend">是</ElTag>
+                <ElTag type="info" v-else>否</ElTag>
+            </template>
+        </ElTableColumn>
+        <ElTableColumn prop="busy" label="忙碌" align="center">
+            <template #default="{ row }">
+                <ElTag type="success" v-if="row.busy">忙碌</ElTag>
+                <ElTag type="info" v-else>空闲</ElTag>
+            </template>
+        </ElTableColumn>
+        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn label="操作" align="center" width="100">
+            <template #default="{ row }">
+                <ElButton type="danger" size="small" @click="del(row)">删除</ElButton>
+            </template>
+        </ElTableColumn>
+    </PagingTable>
+</template>
+<script setup>
+import { ref } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter } from '@/utils/formatter'
+import { Refresh } from '@vicons/tabler'
+import { setupEditDialog } from '@/utils/editDialog'
+import { ElMessageBox, ElMessage } from 'element-plus'
+import { http } from '@/plugins/http'
+
+const where = ref({})
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const rules = {
+    name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+    listId: [{ required: true, message: '请选择料子列表', trigger: 'blur' }]
+}
+const { showEditDialog, onEdit } = setupEditDialog(model)
+const online = ref('')
+const onlineStatus = [
+    {
+        id: '1',
+        label: '在线'
+    },
+    {
+        id: '0',
+        label: '离线'
+    }
+]
+const canSend = ref('')
+const canSendStatus = [
+    {
+        id: '1',
+        label: '是'
+    },
+    {
+        id: '0',
+        label: '否'
+    }
+]
+const busy = ref('')
+const busyStatus = [
+    {
+        id: '1',
+        label: '忙碌'
+    },
+    {
+        id: '0',
+        label: '空闲'
+    }
+]
+
+function updateWhereAndRefresh() {
+    where.value = {}
+    if (online.value) {
+        where.value.online = online.value
+    }
+    if (canSend.value) {
+        where.value.canSend = canSend.value
+    }
+    if (busy.value) {
+        where.value.busy = busy.value
+    }
+}
+
+async function del(row) {
+    await ElMessageBox.confirm('确定删除吗?', '提示', {
+        type: 'warning'
+    })
+    try {
+        await http.delete(`/device/${row.id}`)
+        table.value.refresh()
+    } catch (e) {
+        console.error(e)
+        ElMessage.error(e.message || '删除失败')
+    }
+}
+</script>

+ 5 - 0
src/views/HomeView.vue

@@ -0,0 +1,5 @@
+<template>
+    <el-main> </el-main>
+</template>
+
+<script setup></script>

+ 58 - 0
src/views/LoginView.vue

@@ -0,0 +1,58 @@
+<template>
+    <ElContainer class="h-full bg-cover bg-center" @keyup.enter="login" :style="{ backgroundImage: `url(${bg})` }">
+        <ElMain class="backdrop-blur1 dark:backdrop-brightness-50 !flex flex-col items-center justify-center">
+            <span class="text-3xl font-[sh] text-white [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">RCS</span>
+            <ElCard class="w-full max-w-lg !rounded-xl mt-8 mb-16">
+                <ElForm :model="model" :rules="rules" label-position="top" ref="form">
+                    <ElFormItem prop="username" label="用户名">
+                        <ElInput v-model="model.username"></ElInput>
+                    </ElFormItem>
+                    <ElFormItem prop="username" label="密码">
+                        <ElInput type="password" v-model="model.password"></ElInput>
+                    </ElFormItem>
+                </ElForm>
+                <el-button class="mt-8 w-full !block" type="primary" @click="login" :loading="loading">登录</el-button>
+            </ElCard>
+            <div class="fixed top-0 right-0 px-4 py-4">
+                <DarkSwitch />
+            </div>
+        </ElMain>
+    </ElContainer>
+</template>
+<script setup>
+import { ref } from 'vue'
+import DarkSwitch from '@/components/DarkSwitch.vue'
+import { http } from '@/plugins/http'
+import { useUserStore } from '@/stores/user'
+import { storeToRefs } from 'pinia'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import bg from '@/assets/2.jpg'
+
+const router = useRouter()
+const { user, setUser } = storeToRefs(useUserStore())
+const model = ref({
+    username: '',
+    password: ''
+})
+const rules = {
+    username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+    password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
+}
+const form = ref(null)
+const loading = ref(false)
+function login() {
+    form.value.validate().then(async () => {
+        loading.value = true
+        try {
+            const res = await http.post('/auth/admin/login', model.value)
+            http.setToken(res.access_token)
+            router.replace({ name: 'home' })
+            loading.value = false
+        } catch (e) {
+            loading.value = false
+            ElMessage.error(e.message)
+        }
+    })
+}
+</script>

+ 205 - 0
src/views/MainView.vue

@@ -0,0 +1,205 @@
+<template>
+    <ElContainer class="h-full">
+        <ElAside class="bg-slate-100 dark:bg-zinc-800" v-if="!isMobile">
+            <div class="h-16 px-4 flex items-center justify-center cursor-pointer">
+                <span class="text-lg font-[sh]">RCS</span>
+            </div>
+            <SideMenu :default-active="activeMenu" :menus="menus" />
+        </ElAside>
+        <ElContainer>
+            <ElHeader class="!h-16 border-b !box-border border-neutral-200 dark:border-neutral-800 flex items-center">
+                <div class="p-4 mr-4 cursor-pointer" @click="toggleMenu" v-if="isMobile">
+                    <Menu2 class="w-5 h-5" />
+                </div>
+
+                <el-breadcrumb separator="/">
+                    <el-breadcrumb-item :to="{ path: '/' }">主页</el-breadcrumb-item>
+                    <el-breadcrumb-item v-if="route.name !== 'home'"
+                        >{{ route.meta?.title || route.path }}
+                    </el-breadcrumb-item>
+                </el-breadcrumb>
+                <div class="grow"></div>
+                <!-- <div class="balance-display text-sm mr-4 text-neutral-600 dark:text-neutral-400" v-if="user.roles.includes('user')">
+                    已发送: {{ user.send }}, 余额: {{ user.balance }}
+                </div> -->
+                <DarkSwitch class="mr-4" />
+                <el-dropdown @command="onCommand">
+                    <UserAvatar />
+                    <template #dropdown>
+                        <el-dropdown-menu>
+                            <el-dropdown-item command="changePassword">修改密码</el-dropdown-item>
+                            <el-dropdown-item command="logout">退出登录</el-dropdown-item>
+                        </el-dropdown-menu>
+                    </template>
+                </el-dropdown>
+            </ElHeader>
+            <ElMain class="bg-neutral-50 dark:bg-neutral-900" id="main-container">
+                <RouterView></RouterView>
+            </ElMain>
+        </ElContainer>
+
+        <ElDrawer
+            v-model="showDrawer"
+            direction="ltr"
+            size="80%"
+            v-if="isMobile"
+            class="!bg-slate-100 dark:!bg-zinc-800"
+        >
+            <SideMenu :default-active="activeMenu" :menus="menus" />
+        </ElDrawer>
+
+        <ChangePwd v-model="showChangePwdDialog"></ChangePwd>
+    </ElContainer>
+</template>
+<script setup>
+import { ElAside, ElContainer, ElHeader, ElMain, ElMessageBox } from 'element-plus'
+import DarkSwitch from '@/components/DarkSwitch.vue'
+import SideMenu from '@/components/SideMenu.vue'
+import { useRoute } from 'vue-router'
+import { ref, watch, shallowRef, inject } from 'vue'
+import { User, Home, Menu2, Settings, MessageDots, DeviceMobileMessage } from '@vicons/tabler'
+import UserAvatar from '@/components/UserAvatar.vue'
+import ChangePwd from '@/components/ChangePwd.vue'
+import { http } from '@/plugins/http'
+import { useUserStore } from '@/stores/user'
+
+const route = useRoute()
+const activeMenu = ref(route.path || '/home')
+const isMobile = inject('isMobile')
+const showDrawer = ref(false)
+const { user } = useUserStore()
+const roles = user.roles
+let menus = []
+if (roles.includes('admin')) {
+    menus = [
+        {
+            name: '/home',
+            title: '主页',
+            icon: Home
+        },
+        {
+            name: 'rcs-parent',
+            title: 'RCS管理',
+            icon: DeviceMobileMessage,
+            children: [
+                {
+                    name: '/phoneList',
+                    title: '料子列表'
+                },
+                {
+                    name: '/task',
+                    title: '任务列表'
+                },
+                {
+                    name: '/device',
+                    title: '设备列表'
+                },
+                {
+                    name: '/channel',
+                    title: '渠道列表'
+                },
+                {
+                    name: '/rcsNumber',
+                    title: 'RCS号码'
+                }
+            ]
+        },
+        {
+            name: 'user-parent',
+            title: '用户管理',
+            icon: User,
+            children: [
+                {
+                    name: '/user',
+                    title: '用户列表'
+                },
+                {
+                    name: '/dealer',
+                    title: '经销商列表'
+                }
+            ]
+        },
+        {
+            name: 'settings',
+            title: '系统设置',
+            icon: Settings,
+            children: [
+                {
+                    name: '/sysConfig',
+                    title: '参数设置'
+                }
+            ]
+        }
+    ]
+} else if (roles.includes('api')) {
+    menus = [
+        {
+            name: '/home',
+            title: '主页',
+            icon: Home
+        },
+        {
+            name: 'user-parent',
+            title: '用户管理',
+            icon: User,
+            children: [
+                {
+                    name: '/dealer',
+                    title: '经销商列表'
+                }
+            ]
+        }
+    ]
+} else if (roles.includes('user')) {
+    menus = [
+        {
+            name: '/home',
+            title: '主页',
+            icon: Home
+        },
+        {
+            name: 'rcs-parent',
+            title: 'RCS管理',
+            icon: DeviceMobileMessage,
+            children: [
+                {
+                    name: '/phoneList',
+                    title: '料子列表'
+                },
+                {
+                    name: '/task',
+                    title: '任务列表'
+                }
+            ]
+        }
+    ]
+}
+
+function toggleMenu() {
+    showDrawer.value = !showDrawer.value
+}
+
+watch(route, () => {
+    activeMenu.value = route.name
+})
+const showChangePwdDialog = ref(false)
+
+function onCommand(cmd) {
+    if (cmd === 'logout') {
+        logout()
+    } else if (cmd === 'changePassword') {
+        showChangePwdDialog.value = true
+    }
+}
+
+function logout() {
+    ElMessageBox.confirm('确定退出登录吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+    }).then(() => {
+        http.setToken(null)
+        location.reload()
+    })
+}
+</script>

+ 4 - 0
src/views/NotFoundView.vue

@@ -0,0 +1,4 @@
+<template>
+    <div></div>
+</template>
+<script setup></script>

+ 161 - 0
src/views/PhoneListView.vue

@@ -0,0 +1,161 @@
+<template>
+    <PagingTable url="/phone-list" :where="where" ref="table">
+        <template #filter>
+            <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
+        </template>
+        <ElTableColumn prop="id" label="#" width="80" />
+        <ElTableColumn prop="name" label="名称" min-width="120" />
+        <ElTableColumn prop="remark" label="备注" show-overflow-tooltip />
+        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn label="操作" align="center" width="200">
+            <template #default="{ row }">
+                <ElButton type="primary" size="small" @click="detail(row)">详情</ElButton>
+                <ElButton type="danger" size="small" @click="del(row)">删除</ElButton>
+                <ElButton @click="download(row)" v-if="shouldShow">下载</ElButton>
+            </template>
+        </ElTableColumn>
+    </PagingTable>
+    <EditDialog v-model="showEditDialog" :model="model" :rules="rules" :on-submit="submit" @success="table.refresh()">
+        <ElFormItem prop="name" label="名称">
+            <ElInput v-model="model.name" placeholder="请输入名称" />
+        </ElFormItem>
+        <ElFormItem prop="remark" label="备注">
+            <ElInput v-model="model.remark" placeholder="请输入备注" />
+        </ElFormItem>
+    </EditDialog>
+
+    <ElDialog v-model="showDetailDialog" title="详情" width="800px">
+        <PagingTable url="/phone-list/phone" :where="{ listId: (selectedRow || {}).id }" ref="phoneTable" height="50vh">
+            <template #filter>
+                <ElButton :icon="Plus" @click="onPhoneEdit()">添加</ElButton>
+                <ElButton :icon="Plus" @click="importList()" :loading="importing">导入</ElButton>
+                <div class="align-middle inline-block text-sm text-neutral-600 dark:text-neutral-400">
+                    txt文件,一行一个号码
+                </div>
+            </template>
+            <ElTableColumn prop="id" label="#" width="80" />
+            <ElTableColumn prop="number" label="号码" min-width="120" />
+            <ElTableColumn label="操作" align="center" width="200">
+                <template #default="{ row }">
+                    <ElButton type="primary" size="small" @click="onPhoneEdit(row)">编辑</ElButton>
+                    <ElButton type="danger" size="small" @click="delPhone(row)">删除</ElButton>
+                </template>
+            </ElTableColumn>
+        </PagingTable>
+    </ElDialog>
+
+    <EditDialog
+        v-model="showPhoneEditDialog"
+        :model="phoneModel"
+        :rules="phoneRules"
+        :on-submit="submitPhone"
+        @success="phoneTable.refresh()"
+    >
+        <ElFormItem prop="number" label="号码">
+            <ElInput v-model="phoneModel.number" placeholder="号码" />
+        </ElFormItem>
+    </EditDialog>
+</template>
+<script setup>
+import { ref, computed } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter } from '@/utils/formatter'
+import { Plus } from '@vicons/tabler'
+import EditDialog from '@/components/EditDialog.vue'
+import { setupEditDialog } from '@/utils/editDialog'
+import EnumSelect from '@/components/EnumSelect.vue'
+import { UserRole } from '@/enums'
+import { axiosInstance, http } from '@/plugins/http'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useClipboard } from '@vueuse/core'
+import { useUserStore } from '@/stores/user'
+import { storeToRefs } from 'pinia'
+
+const where = ref({})
+const { user } = storeToRefs(useUserStore())
+const shouldShow = computed(() => user.value.roles.includes('admin'))
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const rules = {
+    name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
+}
+const { showEditDialog, onEdit } = setupEditDialog(model)
+async function submit() {
+    await http.put('/phone-list', model.value)
+    ElMessage.success('保存成功')
+}
+function del(row) {
+    ElMessageBox.confirm('确定删除吗?', '提示', {
+        type: 'warning'
+    }).then(() => {
+        http.delete(`/phone-list/${row.id}`).then(() => {
+            ElMessage.success('删除成功')
+            table.value.refresh()
+        })
+    })
+}
+
+const selectedRow = ref(null)
+const showDetailDialog = ref(false)
+const phoneTable = ref(null)
+function detail(row) {
+    selectedRow.value = row
+    showDetailDialog.value = true
+}
+
+const phoneModel = ref({})
+const phoneRules = {
+    number: [{ required: true, message: '请输入号码', trigger: 'blur' }]
+}
+const { showEditDialog: showPhoneEditDialog, onEdit: onPhoneEdit } = setupEditDialog(phoneModel)
+async function submitPhone() {
+    phoneModel.value.listId = selectedRow.value.id
+    await http.put('/phone-list/phone', phoneModel.value)
+    ElMessage.success('保存成功')
+}
+function delPhone(row) {
+    ElMessageBox.confirm('确定删除吗?', '提示', {
+        type: 'warning'
+    }).then(() => {
+        http.delete(`/phone-list/phone/${row.id}`).then(() => {
+            ElMessage.success('删除成功')
+            phoneTable.value.refresh()
+        })
+    })
+}
+const importing = ref(false)
+function importList() {
+    const input = document.createElement('input')
+    input.type = 'file'
+    input.accept = '.txt'
+    input.onchange = async () => {
+        const file = input.files[0]
+        const formData = new FormData()
+        formData.append('file', file)
+        try {
+            importing.value = true
+            await http.post(`/phone-list/${selectedRow.value.id}/import`, formData)
+            ElMessage.success('导入成功')
+        } catch (e) {
+            ElMessage.error(e.message || '导入失败')
+        }
+        importing.value = false
+        phoneTable.value.refresh()
+    }
+    input.click()
+}
+
+function download(row) {
+    axiosInstance.get(`/phone-list/${row.id}/download`, { responseType: 'blob' }).then((res) => {
+        const url = window.URL.createObjectURL(new Blob([res.data]))
+        const link = document.createElement('a')
+        link.style.display = 'none'
+        link.href = url
+        link.setAttribute('download', `${row.number}.txt`)
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+    })
+}
+</script>

+ 77 - 0
src/views/RcsNumberView.vue

@@ -0,0 +1,77 @@
+<template>
+    <PagingTable url="/rcs-number" :where="where" ref="table">
+        <template #filter>
+            <ElButton :icon="Search" @click="table.refresh()"></ElButton>
+            <ElSelect v-model="channel" placeholder="渠道" clearable value-key="id">
+                <ElOption v-for="channel in channels" :key="channel.id" :label="channel.country" :value="channel" />
+            </ElSelect>
+            <ElButton :icon="Plus" @click="getNumber()">取号</ElButton>
+        </template>
+        <ElTableColumn prop="id" label="#" width="80" />
+        <ElTableColumn prop="mcc" label="MCC" />
+        <ElTableColumn prop="mnc" label="MNC" />
+        <ElTableColumn prop="country" label="国家" />
+        <ElTableColumn prop="number" label="号码" />
+        <ElTableColumn prop="message" label="消息" show-overflow-tooltip />
+        <ElTableColumn prop="taskId" label="taskId" align="center" width="80"/>
+        <ElTableColumn label="状态" align="center" width="80">
+            <template #default="{ row }">
+                <ElTag v-if="row.status === 'pending'" type="info">等待</ElTag>
+                <ElTag v-else-if="row.status === 'success'" type="success">成功</ElTag>
+                <ElTag v-else-if="row.status === 'expired'" type="danger">过期</ElTag>
+                <ElTag v-else-if="row.status === 'error'" type="danger">错误</ElTag>
+            </template>
+        </ElTableColumn>
+        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn label="操作" align="center" width="120">
+            <template #default="{}"></template>
+        </ElTableColumn>
+    </PagingTable>
+</template>
+<script setup>
+import { computed, onMounted, ref } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter } from '@/utils/formatter'
+import { Plus, Search } from '@vicons/tabler'
+import EditDialog from '@/components/EditDialog.vue'
+import { setupEditDialog } from '@/utils/editDialog'
+import EnumSelect from '@/components/EnumSelect.vue'
+import { UserRole } from '@/enums'
+import { http } from '@/plugins/http'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useClipboard } from '@vueuse/core'
+
+const where = computed(() => ({ country: channel.value?.country }))
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const rules = {
+    username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+    name: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
+    phone: [{ required: true, message: '请输入手机', trigger: 'blur' }],
+    password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+    roles: [{ required: true, message: '请选择角色', trigger: 'blur' }]
+}
+const channels = ref([])
+const channel = ref(null)
+
+async function getNumber() {
+    await ElMessageBox.confirm('确定取号吗?')
+    try {
+        await http.put('/rcs-number', {
+            channelId: channel.value?.id
+        })
+        ElMessage.success('取号成功')
+        table.value.refresh()
+    } catch (error) {
+        console.error(error)
+        ElMessage.error(error.message || '取号失败')
+    }
+}
+
+onMounted(() => {
+    http.post('/channel').then((res) => {
+        channels.value = res.items
+    })
+})
+</script>

+ 75 - 0
src/views/SysConfigView.vue

@@ -0,0 +1,75 @@
+<template>
+    <PagingTable url="/sys-config" :where="where" ref="table">
+        <template #filter>
+            <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
+        </template>
+        <ElTableColumn prop="id" label="#" width="80" />
+        <ElTableColumn prop="name" label="名称" width="150" />
+        <ElTableColumn prop="remark" label="备注" min-width="120" show-overflow-tooltip />
+        <ElTableColumn prop="value" label="值" min-width="120" show-overflow-tooltip />
+        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn label="操作" align="center" width="120">
+            <template #default="{ row }">
+                <ElButton @click="onEdit(row)">编辑</ElButton>
+            </template>
+        </ElTableColumn>
+    </PagingTable>
+    <EditDialog v-model="showEditDialog" :model="model" :rules="rules" :on-submit="submit" @success="table.refresh()">
+        <ElFormItem prop="name" label="名称">
+            <ElInput v-model="model.name" placeholder="请输入名称" />
+        </ElFormItem>
+        <ElFormItem prop="remark" label="备注">
+            <ElInput v-model="model.remark" placeholder="请输入备注" />
+        </ElFormItem>
+        <ElFormItem prop="value" label="值" v-if="model.name === 'model'">
+            <ElSelect v-model="model.value">
+                <ElOption label="gpt-3.5" value="gpt-3.5" />
+                <ElOption label="gpt-4" value="gpt-4" />
+                <ElOption label="azure-gpt-3.5" value="azure-gpt-3.5" />
+                <ElOption label="azure-gpt-4" value="azure-gpt-4" />
+                <ElOption label="cf-gpt-3.5" value="cf-gpt-3.5" />
+                <ElOption label="cf-gpt-4" value="cf-gpt-4" />
+            </ElSelect>
+        </ElFormItem>
+        <ElFormItem prop="value" label="值" v-else>
+            <ElInput v-model="model.value" placeholder="请输入值" />
+        </ElFormItem>
+    </EditDialog>
+</template>
+<script setup>
+import { ref } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter } from '@/utils/formatter'
+import { Plus } from '@vicons/tabler'
+import EditDialog from '@/components/EditDialog.vue'
+import { setupEditDialog } from '@/utils/editDialog'
+import EnumSelect from '@/components/EnumSelect.vue'
+import { UserRole } from '@/enums'
+import { http } from '@/plugins/http'
+import { ElMessage } from 'element-plus'
+import { useClipboard } from '@vueuse/core'
+
+const where = ref({})
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const rules = {
+    name: [
+        { required: true, message: '请输入名称', trigger: 'blur' },
+        { type: 'string', pattern: /^[a-zA-Z0-9_]+$/, message: '只能输入字母、数字、下划线' }
+    ],
+    value: [{ required: true, message: '请输入值', trigger: 'blur' }]
+}
+const { showEditDialog, onEdit } = setupEditDialog(model)
+async function submit() {
+    await http.put('/admin/sys-config', model.value)
+    ElMessage.success('保存成功')
+}
+function getToken(row) {
+    http.get(`/auth/admin/user/${row.id}/token`).then((res) => {
+        const { copy } = useClipboard({ legacy: true })
+        copy(res.access_token)
+        ElMessage.success('复制成功')
+    })
+}
+</script>

+ 217 - 0
src/views/TaskView.vue

@@ -0,0 +1,217 @@
+<template>
+    <PagingTable url="/task" :where="where" ref="table">
+        <template #filter>
+            <ElButton :icon="Refresh" @click="table.refresh()"></ElButton>
+            <ElButton :icon="Plus" @click="onEdit(), getPhoneList()"> 添加</ElButton>
+        </template>
+        <ElTableColumn prop="id" label="#" width="80" />
+        <ElTableColumn prop="name" label="名称" min-width="120" />
+        <ElTableColumn prop="remark" label="备注" show-overflow-tooltip />
+        <ElTableColumn prop="message" label="内容" show-overflow-tooltip />
+        <ElTableColumn prop="listId" label="料子" :formatter="phoneListFormatter" />
+        <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn prop="timed" label="定时" :formatter="timeFormatter" width="150" />
+        <ElTableColumn label="操作" align="center" width="100">
+            <template #default="{ row }">
+                <ElButton type="danger" size="small" @click="del(row)" v-if="row.status === 'idle'">删除</ElButton>
+            </template>
+        </ElTableColumn>
+    </PagingTable>
+    <EditDialog
+        v-model="showEditDialog"
+        :model="model"
+        :rules="rules"
+        :on-submit="submit"
+        @success="table.refresh()"
+        label-width="120px"
+    >
+        <ElFormItem prop="name" label="名称">
+            <ElInput v-model="model.name" placeholder="请输入名称" />
+        </ElFormItem>
+        <ElFormItem prop="remark" label="备注">
+            <ElInput v-model="model.remark" placeholder="请输入备注" />
+        </ElFormItem>
+        <ElFormItem prop="listId" label="料子">
+            <ElSelect v-model="model.listId" placeholder="请选择料子">
+                <ElOption v-for="item in phoneList" :key="item.id" :label="item.name" :value="item.id" />
+            </ElSelect>
+        </ElFormItem>
+        <ElFormItem prop="message" label="内容">
+            <ElInput v-model="model.message" placeholder="请输入内容" type="textarea" />
+        </ElFormItem>
+        <ElFormItem prop="timed" label="定时">
+            <ElDatePicker v-model="model.timed" type="datetime" placeholder="请选择定时时间" />
+        </ElFormItem>
+    </EditDialog>
+</template>
+<script setup>
+import { computed, ref } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter } from '@/utils/formatter'
+import { Plus, Refresh } from '@vicons/tabler'
+import EditDialog from '@/components/EditDialog.vue'
+import { setupEditDialog } from '@/utils/editDialog'
+import { http } from '@/plugins/http'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useUserStore } from '@/stores/user'
+import { storeToRefs } from 'pinia'
+
+const where = ref({})
+const { user } = storeToRefs(useUserStore())
+const shouldShow = computed(() => user.value.roles.includes('admin'))
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const rules = {
+    name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+    listId: [{ required: true, message: '请选择料子', trigger: 'blur' }],
+    message: [{ required: true, message: '请输入内容', trigger: 'blur' }],
+    rcsWait: [{ required: false, message: '请输入RCS等待时间', trigger: 'blur' }],
+    rcsInterval: [{ required: false, message: '请输入RCS发送间隔', trigger: 'blur' }],
+    cleanCount: [{ required: false, message: '请输入清理数量', trigger: 'blur' }],
+    requestNumberInterval: [{ required: false, message: '请输入请求号码间隔', trigger: 'blur' }]
+}
+const { showEditDialog, onEdit } = setupEditDialog(model)
+
+async function submit() {
+    model.value.userId = user.id
+    if (model.value.channelId) {
+        model.value.channelId = model.value.channelId.join(',')
+    }
+    await http.put('/task', model.value)
+    ElMessage.success('保存成功')
+}
+
+const phoneList = ref([])
+
+async function getPhoneList() {
+    phoneList.value = (
+        await http.post('/phone-list', {
+            search: { page: { page: 1, limit: 100 }, order: { createdAt: 'DESC' }, where: where.value }
+        })
+    ).items
+}
+
+getPhoneList()
+const operatorList = ref([])
+
+async function getOperatorList() {
+    operatorList.value = (await http.post('/channel')).items
+}
+
+getOperatorList()
+
+function phoneListFormatter(row, column, cellValue, index) {
+    return phoneList.value.find((item) => item.id === cellValue)?.name
+}
+
+function del(row) {
+    ElMessageBox.confirm('确定删除吗?', '提示', {
+        type: 'warning'
+    }).then(() => {
+        http.delete(`/task/${row.id}`).then(() => {
+            ElMessage.success('删除成功')
+            table.value.refresh()
+        })
+    })
+}
+
+const selectedRow = ref(null)
+const showDetailDialog = ref(false)
+const phoneTable = ref(null)
+
+function detail(row) {
+    selectedRow.value = row
+    showDetailDialog.value = true
+}
+
+const phoneModel = ref({})
+const phoneRules = {
+    number: [{ required: true, message: '请输入号码', trigger: 'blur' }]
+}
+const { showEditDialog: showPhoneEditDialog, onEdit: onPhoneEdit } = setupEditDialog(phoneModel)
+
+function taskStatusFormatter(row, column, cellValue, index) {
+    switch (cellValue) {
+        case 'idle':
+            return '未发送'
+        case 'pending':
+            return '发送中'
+        case 'completed':
+            return '已发送'
+        case 'error':
+            return '错误'
+        default:
+            return '未知'
+    }
+}
+
+function taskItemStatusFormatter(row, column, cellValue, index) {
+    switch (cellValue) {
+        case 'idle':
+            return '未发送'
+        case 'pending':
+            return '发送中'
+        case 'success':
+            return '成功'
+        case 'fail':
+            return '失败'
+        default:
+            return '未知'
+    }
+}
+
+async function start(row) {
+    const cost = await http.get(`/task/verification/${row.id}`)
+    if (cost > 0 && row.status === 'idle') {
+        const confirm = await ElMessageBox.confirm(`该任务需要从余额中扣除 ${cost} ,是否继续?`, '提示', {
+            type: 'warning'
+        })
+        if (confirm) {
+            await http.post(`/task/${row.id}/start`)
+            table.value.refresh()
+        }
+    } else if (cost < 0 && row.status === 'idle') {
+        await ElMessageBox.alert('余额不足,请充值后再启动任务.', '提示')
+        table.value.refresh()
+    } else {
+        await ElMessageBox.confirm('确定开始发送吗?', '提示', {
+            type: 'warning'
+        })
+        await http.post(`/task/${row.id}/start`)
+        table.value.refresh()
+    }
+}
+
+async function pause(row) {
+    await ElMessageBox.confirm('确定暂停发送吗?', '提示', {
+        type: 'warning'
+    })
+    await http.post(`/task/${row.id}/pause`)
+    table.value.refresh()
+}
+
+async function receipt(row) {
+    try {
+        const response = await http.post(`/task/item/${row.id}/receipt`)
+        const uint8Array = new Uint8Array(response.data)
+        const blob = new Blob([uint8Array], { type: 'application/octet-stream' })
+        const link = document.createElement('a')
+        const blobUrl = URL.createObjectURL(blob)
+        link.href = blobUrl
+        link.download = `${row.name}_回执.xlsx`
+        link.click()
+        window.URL.revokeObjectURL(blobUrl)
+    } catch (error) {
+        console.error('Error receipt:', error)
+        ElMessage.error('获取回执错误,请稍后再试.')
+    }
+}
+</script>
+<style lang="less" scoped>
+.tip {
+    color: #999;
+    font-size: 12px;
+    margin-left: 10px;
+}
+</style>

+ 73 - 0
src/views/UserView.vue

@@ -0,0 +1,73 @@
+<template>
+    <PagingTable url="/admin/users" :where="where" ref="table">
+        <template #filter>
+            <EnumSelect :enum="UserRole" v-model="where.roles"></EnumSelect>
+            <ElButton :icon="Plus" @click="onEdit()">添加</ElButton>
+        </template>
+        <ElTableColumn prop="id" label="#" width="80" />
+        <ElTableColumn prop="username" label="用户名" min-width="120" />
+        <ElTableColumn prop="name" label="昵称" min-width="120" />
+        <ElTableColumn prop="phone" label="手机" min-width="120" />
+        <ElTableColumn prop="createdAt" label="注册时间" :formatter="timeFormatter" width="150" />
+        <ElTableColumn prop="invitor" label="上级" />
+        <ElTableColumn label="操作" align="center" width="120">
+            <template #default="{ row }">
+                <ElButton @click="getToken(row)">Token</ElButton>
+            </template>
+        </ElTableColumn>
+    </PagingTable>
+    <EditDialog v-model="showEditDialog" :model="model" :rules="rules" :on-submit="submit" @success="table.refresh()">
+        <ElFormItem prop="username" label="用户名">
+            <ElInput v-model="model.username" placeholder="请输入用户名" />
+        </ElFormItem>
+        <ElFormItem prop="name" label="昵称">
+            <ElInput v-model="model.name" placeholder="请输入昵称" />
+        </ElFormItem>
+        <ElFormItem prop="phone" label="手机">
+            <ElInput v-model="model.phone" placeholder="请输入手机" />
+        </ElFormItem>
+        <ElFormItem prop="password" label="密码">
+            <ElInput v-model="model.password" placeholder="请输入密码" />
+        </ElFormItem>
+        <ElFormItem prop="roles" label="角色">
+            <EnumSelect v-model="model.roles" :enum="UserRole" multiple :multiple-limit="1"/>
+        </ElFormItem>
+    </EditDialog>
+</template>
+<script setup>
+import { ref } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter } from '@/utils/formatter'
+import { Plus } from '@vicons/tabler'
+import EditDialog from '@/components/EditDialog.vue'
+import { setupEditDialog } from '@/utils/editDialog'
+import EnumSelect from '@/components/EnumSelect.vue'
+import { UserRole } from '@/enums'
+import { http } from '@/plugins/http'
+import { ElMessage } from 'element-plus'
+import { useClipboard } from '@vueuse/core'
+
+const where = ref({ roles: 'user' })
+const timeFormatter = useTimeFormatter()
+const table = ref(null)
+const model = ref({})
+const rules = {
+    username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+    name: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
+    phone: [{ required: true, message: '请输入手机', trigger: 'blur' }],
+    password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+    roles: [{ required: true, message: '请选择角色', trigger: 'blur' }]
+}
+const { showEditDialog, onEdit } = setupEditDialog(model)
+async function submit() {
+    await http.put('/admin/users', model.value)
+    ElMessage.success('保存成功')
+}
+function getToken(row) {
+    http.get(`/auth/admin/user/${row.id}/token`).then((res) => {
+        const { copy } = useClipboard({ legacy: true })
+        copy(res.access_token)
+        ElMessage.success('复制成功')
+    })
+}
+</script>

+ 9 - 0
tailwind.config.js

@@ -0,0 +1,9 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+    content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
+    theme: {
+        extend: {}
+    },
+    plugins: [],
+    darkMode: 'class'
+}

+ 38 - 0
vite.config.js

@@ -0,0 +1,38 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig, loadEnv } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+
+// https://vitejs.dev/config/
+export default defineConfig(({ command, mode }) => {
+    process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }
+    return {
+        base: process.env.VITE_BASE_URL,
+        build: {
+            sourcemap: false
+        },
+        plugins: [
+            vue(),
+            AutoImport({
+                resolvers: [ElementPlusResolver()]
+            }),
+            Components({
+                resolvers: [ElementPlusResolver()]
+            })
+        ],
+        resolve: {
+            alias: {
+                '@': fileURLToPath(new URL('./src', import.meta.url))
+            }
+        },
+        server: {
+            host: '0.0.0.0'
+        },
+        define: {
+            'process.env': process.env
+        }
+    }
+})

+ 15 - 0
volar.config.js

@@ -0,0 +1,15 @@
+/* eslint-disable no-undef */
+/** @type {import('@volar-plugins/prettier')} */
+const { volarPrettierPlugin } = require('@volar-plugins/prettier')
+
+module.exports = {
+    plugins: [
+        volarPrettierPlugin({
+            languages: ['html', 'css', 'scss', 'typescript', 'javascript'],
+            html: {
+                breakContentsFromTags: true
+            },
+            useVscodeIndentation: true
+        })
+    ]
+}

+ 2233 - 0
yarn.lock

@@ -0,0 +1,2233 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@antfu/utils@^0.7.2":
+  version "0.7.2"
+  resolved "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.2.tgz#3bb6f37a6b188056fe9e2f363b6aa735ed65d7ca"
+  integrity sha512-vy9fM3pIxZmX07dL+VX1aZe7ynZ+YyB0jY+jE6r3hOK6GNY2t6W8rzpFC4tgpbXUYABkFQwgJq2XYXlxbXAI0g==
+
+"@babel/parser@^7.16.4":
+  version "7.21.4"
+  resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17"
+  integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==
+
+"@ctrl/tinycolor@^3.4.1":
+  version "3.6.0"
+  resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz#53fa5fe9c34faee89469e48f91d51a3766108bc8"
+  integrity sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==
+
+"@element-plus/icons-vue@^2.0.6":
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.1.0.tgz#7ad90d08a8c0d5fd3af31c4f73264ca89614397a"
+  integrity sha512-PSBn3elNoanENc1vnCfh+3WA9fimRC7n+fWkf3rE5jvv+aBohNHABC/KAR5KWPecxWxDTVT1ERpRbOMRcOV/vA==
+
+"@esbuild/android-arm64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz#4aa8d8afcffb4458736ca9b32baa97d7cb5861ea"
+  integrity sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==
+
+"@esbuild/android-arm@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.17.18.tgz#74a7e95af4ee212ebc9db9baa87c06a594f2a427"
+  integrity sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==
+
+"@esbuild/android-x64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.17.18.tgz#1dcd13f201997c9fe0b204189d3a0da4eb4eb9b6"
+  integrity sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==
+
+"@esbuild/darwin-arm64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz#444f3b961d4da7a89eb9bd35cfa4415141537c2a"
+  integrity sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==
+
+"@esbuild/darwin-x64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz#a6da308d0ac8a498c54d62e0b2bfb7119b22d315"
+  integrity sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==
+
+"@esbuild/freebsd-arm64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz#b83122bb468889399d0d63475d5aea8d6829c2c2"
+  integrity sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==
+
+"@esbuild/freebsd-x64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz#af59e0e03fcf7f221b34d4c5ab14094862c9c864"
+  integrity sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==
+
+"@esbuild/linux-arm64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz#8551d72ba540c5bce4bab274a81c14ed01eafdcf"
+  integrity sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==
+
+"@esbuild/linux-arm@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz#e09e76e526df4f665d4d2720d28ff87d15cdf639"
+  integrity sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==
+
+"@esbuild/linux-ia32@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz#47878860ce4fe73a36fd8627f5647bcbbef38ba4"
+  integrity sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==
+
+"@esbuild/linux-loong64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz#3f8fbf5267556fc387d20b2e708ce115de5c967a"
+  integrity sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==
+
+"@esbuild/linux-mips64el@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz#9d896d8f3c75f6c226cbeb840127462e37738226"
+  integrity sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==
+
+"@esbuild/linux-ppc64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz#3d9deb60b2d32c9985bdc3e3be090d30b7472783"
+  integrity sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==
+
+"@esbuild/linux-riscv64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz#8a943cf13fd24ff7ed58aefb940ef178f93386bc"
+  integrity sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==
+
+"@esbuild/linux-s390x@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz#66cb01f4a06423e5496facabdce4f7cae7cb80e5"
+  integrity sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==
+
+"@esbuild/linux-x64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz#23c26050c6c5d1359c7b774823adc32b3883b6c9"
+  integrity sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==
+
+"@esbuild/netbsd-x64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz#789a203d3115a52633ff6504f8cbf757f15e703b"
+  integrity sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==
+
+"@esbuild/openbsd-x64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz#d7b998a30878f8da40617a10af423f56f12a5e90"
+  integrity sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==
+
+"@esbuild/sunos-x64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz#ecad0736aa7dae07901ba273db9ef3d3e93df31f"
+  integrity sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==
+
+"@esbuild/win32-arm64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz#58dfc177da30acf956252d7c8ae9e54e424887c4"
+  integrity sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==
+
+"@esbuild/win32-ia32@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz#340f6163172b5272b5ae60ec12c312485f69232b"
+  integrity sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==
+
+"@esbuild/win32-x64@0.17.18":
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz#3a8e57153905308db357fd02f57c180ee3a0a1fa"
+  integrity sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==
+
+"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.3.0":
+  version "4.4.0"
+  resolved "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
+  integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
+  dependencies:
+    eslint-visitor-keys "^3.3.0"
+
+"@eslint-community/regexpp@^4.4.0":
+  version "4.5.0"
+  resolved "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.5.0.tgz#f6f729b02feee2c749f57e334b7a1b5f40a81724"
+  integrity sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==
+
+"@eslint/eslintrc@^2.0.2":
+  version "2.0.2"
+  resolved "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.0.2.tgz#01575e38707add677cf73ca1589abba8da899a02"
+  integrity sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==
+  dependencies:
+    ajv "^6.12.4"
+    debug "^4.3.2"
+    espree "^9.5.1"
+    globals "^13.19.0"
+    ignore "^5.2.0"
+    import-fresh "^3.2.1"
+    js-yaml "^4.1.0"
+    minimatch "^3.1.2"
+    strip-json-comments "^3.1.1"
+
+"@eslint/js@8.39.0":
+  version "8.39.0"
+  resolved "https://registry.npmmirror.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b"
+  integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==
+
+"@floating-ui/core@^1.2.6":
+  version "1.2.6"
+  resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-1.2.6.tgz#d21ace437cc919cdd8f1640302fa8851e65e75c0"
+  integrity sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==
+
+"@floating-ui/dom@^1.0.1":
+  version "1.2.6"
+  resolved "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.2.6.tgz#bcf0c7bada97c20d9d1255b889f35bac838c63fe"
+  integrity sha512-02vxFDuvuVPs22iJICacezYJyf7zwwOCWkPNkWNBr1U0Qt1cKFYzWvxts0AmqcOQGwt/3KJWcWIgtbUU38keyw==
+  dependencies:
+    "@floating-ui/core" "^1.2.6"
+
+"@humanwhocodes/config-array@^0.11.8":
+  version "0.11.8"
+  resolved "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
+  integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==
+  dependencies:
+    "@humanwhocodes/object-schema" "^1.2.1"
+    debug "^4.1.1"
+    minimatch "^3.0.5"
+
+"@humanwhocodes/module-importer@^1.0.1":
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
+  integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
+
+"@humanwhocodes/object-schema@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
+  integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
+
+"@jridgewell/gen-mapping@^0.3.2":
+  version "0.3.3"
+  resolved "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
+  integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.1"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@3.1.0":
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
+  integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
+
+"@jridgewell/set-array@^1.0.1":
+  version "1.1.2"
+  resolved "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/sourcemap-codec@1.4.14":
+  version "1.4.14"
+  resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
+  integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13":
+  version "1.4.15"
+  resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+  integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@jridgewell/trace-mapping@^0.3.9":
+  version "0.3.18"
+  resolved "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6"
+  integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==
+  dependencies:
+    "@jridgewell/resolve-uri" "3.1.0"
+    "@jridgewell/sourcemap-codec" "1.4.14"
+
+"@nodelib/fs.scandir@2.1.5":
+  version "2.1.5"
+  resolved "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+  integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.5"
+    run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.5"
+  resolved "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+  integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
+  version "1.2.8"
+  resolved "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.5"
+    fastq "^1.6.0"
+
+"@popperjs/core@npm:@sxzz/popperjs-es@^2.11.7":
+  version "2.11.7"
+  resolved "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz#a7f69e3665d3da9b115f9e71671dae1b97e13671"
+  integrity sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==
+
+"@rollup/pluginutils@^5.0.2":
+  version "5.0.2"
+  resolved "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33"
+  integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==
+  dependencies:
+    "@types/estree" "^1.0.0"
+    estree-walker "^2.0.2"
+    picomatch "^2.3.1"
+
+"@rushstack/eslint-patch@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728"
+  integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==
+
+"@socket.io/component-emitter@~3.1.0":
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
+  integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
+
+"@types/estree@^1.0.0":
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
+  integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
+
+"@types/lodash-es@^4.17.6":
+  version "4.17.7"
+  resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.7.tgz#22edcae9f44aff08546e71db8925f05b33c7cc40"
+  integrity sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==
+  dependencies:
+    "@types/lodash" "*"
+
+"@types/lodash@*", "@types/lodash@^4.14.182":
+  version "4.14.194"
+  resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76"
+  integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==
+
+"@types/web-bluetooth@^0.0.16":
+  version "0.0.16"
+  resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8"
+  integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==
+
+"@vicons/tabler@^0.12.0":
+  version "0.12.0"
+  resolved "https://registry.npmmirror.com/@vicons/tabler/-/tabler-0.12.0.tgz#11924d2288e9346d47b44dd643ac20e72a32e089"
+  integrity sha512-3+wUFuxb7e8OzZ8Wryct1pzfA2vyoF4lwW98O9s27ZrfCGaJGNmqG+q8A7vQ92Mf+COCgxpK+rhNPTtTvaU6qw==
+
+"@vitejs/plugin-vue@^4.0.0":
+  version "4.1.0"
+  resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.1.0.tgz#b6a9d83cd91575f7ee15593f6444397f68751073"
+  integrity sha512-++9JOAFdcXI3lyer9UKUV4rfoQ3T1RN8yDqoCLar86s0xQct5yblxAE+yWgRnU5/0FOlVCpTZpYSBV/bGWrSrQ==
+
+"@volar-plugins/prettier@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.npmmirror.com/@volar-plugins/prettier/-/prettier-2.0.0.tgz#9fe8dce0207329de0aee896055c3d74f991a040a"
+  integrity sha512-TRJJrsqzDceEHlM/nL5waaadzBpJHtI/Au83faULXGgK5Hcc8GYuHzDJzOvCyvXgX/pWL6Z+NYgwHbh3TW3UMg==
+
+"@vue/compiler-core@3.2.47":
+  version "3.2.47"
+  resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.47.tgz#3e07c684d74897ac9aa5922c520741f3029267f8"
+  integrity sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==
+  dependencies:
+    "@babel/parser" "^7.16.4"
+    "@vue/shared" "3.2.47"
+    estree-walker "^2.0.2"
+    source-map "^0.6.1"
+
+"@vue/compiler-dom@3.2.47":
+  version "3.2.47"
+  resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz#a0b06caf7ef7056939e563dcaa9cbde30794f305"
+  integrity sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==
+  dependencies:
+    "@vue/compiler-core" "3.2.47"
+    "@vue/shared" "3.2.47"
+
+"@vue/compiler-sfc@3.2.47":
+  version "3.2.47"
+  resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz#1bdc36f6cdc1643f72e2c397eb1a398f5004ad3d"
+  integrity sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==
+  dependencies:
+    "@babel/parser" "^7.16.4"
+    "@vue/compiler-core" "3.2.47"
+    "@vue/compiler-dom" "3.2.47"
+    "@vue/compiler-ssr" "3.2.47"
+    "@vue/reactivity-transform" "3.2.47"
+    "@vue/shared" "3.2.47"
+    estree-walker "^2.0.2"
+    magic-string "^0.25.7"
+    postcss "^8.1.10"
+    source-map "^0.6.1"
+
+"@vue/compiler-ssr@3.2.47":
+  version "3.2.47"
+  resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz#35872c01a273aac4d6070ab9d8da918ab13057ee"
+  integrity sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==
+  dependencies:
+    "@vue/compiler-dom" "3.2.47"
+    "@vue/shared" "3.2.47"
+
+"@vue/devtools-api@^6.4.5", "@vue/devtools-api@^6.5.0":
+  version "6.5.0"
+  resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07"
+  integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
+
+"@vue/eslint-config-prettier@^7.1.0":
+  version "7.1.0"
+  resolved "https://registry.npmmirror.com/@vue/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz#97936379c7fb1d982b9d2c6b122306e3c2e464c8"
+  integrity sha512-Pv/lVr0bAzSIHLd9iz0KnvAr4GKyCEl+h52bc4e5yWuDVtLgFwycF7nrbWTAQAS+FU6q1geVd07lc6EWfJiWKQ==
+  dependencies:
+    eslint-config-prettier "^8.3.0"
+    eslint-plugin-prettier "^4.0.0"
+
+"@vue/reactivity-transform@3.2.47":
+  version "3.2.47"
+  resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz#e45df4d06370f8abf29081a16afd25cffba6d84e"
+  integrity sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==
+  dependencies:
+    "@babel/parser" "^7.16.4"
+    "@vue/compiler-core" "3.2.47"
+    "@vue/shared" "3.2.47"
+    estree-walker "^2.0.2"
+    magic-string "^0.25.7"
+
+"@vue/reactivity@3.2.47":
+  version "3.2.47"
+  resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.47.tgz#1d6399074eadfc3ed35c727e2fd707d6881140b6"
+  integrity sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==
+  dependencies:
+    "@vue/shared" "3.2.47"
+
+"@vue/runtime-core@3.2.47":
+  version "3.2.47"
+  resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.47.tgz#406ebade3d5551c00fc6409bbc1eeb10f32e121d"
+  integrity sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==
+  dependencies:
+    "@vue/reactivity" "3.2.47"
+    "@vue/shared" "3.2.47"
+
+"@vue/runtime-dom@3.2.47":
+  version "3.2.47"
+  resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz#93e760eeaeab84dedfb7c3eaf3ed58d776299382"
+  integrity sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==
+  dependencies:
+    "@vue/runtime-core" "3.2.47"
+    "@vue/shared" "3.2.47"
+    csstype "^2.6.8"
+
+"@vue/server-renderer@3.2.47":
+  version "3.2.47"
+  resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.47.tgz#8aa1d1871fc4eb5a7851aa7f741f8f700e6de3c0"
+  integrity sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==
+  dependencies:
+    "@vue/compiler-ssr" "3.2.47"
+    "@vue/shared" "3.2.47"
+
+"@vue/shared@3.2.47":
+  version "3.2.47"
+  resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.47.tgz#e597ef75086c6e896ff5478a6bfc0a7aa4bbd14c"
+  integrity sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==
+
+"@vueuse/core@^10.1.0":
+  version "10.1.0"
+  resolved "https://registry.npmmirror.com/@vueuse/core/-/core-10.1.0.tgz#7c3246bea35b24298040b2576de06ce87f38f4c6"
+  integrity sha512-3Znoa5m5RO+z4/C9w6DRaKTR3wCVJvD5rav8HTDGsr+7rOZRHtcgFJ8NcCs0ZvIpmev2kExTa311ns5j2RbzDQ==
+  dependencies:
+    "@types/web-bluetooth" "^0.0.16"
+    "@vueuse/metadata" "10.1.0"
+    "@vueuse/shared" "10.1.0"
+    vue-demi ">=0.14.0"
+
+"@vueuse/core@^9.1.0":
+  version "9.13.0"
+  resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz#2f69e66d1905c1e4eebc249a01759cf88ea00cf4"
+  integrity sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==
+  dependencies:
+    "@types/web-bluetooth" "^0.0.16"
+    "@vueuse/metadata" "9.13.0"
+    "@vueuse/shared" "9.13.0"
+    vue-demi "*"
+
+"@vueuse/metadata@10.1.0":
+  version "10.1.0"
+  resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.1.0.tgz#041ab49abb17e760170606199c612c8937d2968f"
+  integrity sha512-cM28HjDEw5FIrPE9rgSPFZvQ0ZYnOLAOr8hl1XM6tFl80U3WAR5ROdnAqiYybniwP5gt9MKKAJAqd/ab2aHkqg==
+
+"@vueuse/metadata@9.13.0":
+  version "9.13.0"
+  resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz#bc25a6cdad1b1a93c36ce30191124da6520539ff"
+  integrity sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==
+
+"@vueuse/shared@10.1.0":
+  version "10.1.0"
+  resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.1.0.tgz#1158713a69d33b84c473dbd078c0e8c587e6ab5f"
+  integrity sha512-2X52ogu12i9DkKOQ01yeb/BKg9UO87RNnpm5sXkQvyORlbq8ONS5l39MYkjkeVWWjdT0teJru7a2S41dmHmqjQ==
+  dependencies:
+    vue-demi ">=0.14.0"
+
+"@vueuse/shared@9.13.0":
+  version "9.13.0"
+  resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz#089ff4cc4e2e7a4015e57a8f32e4b39d096353b9"
+  integrity sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==
+  dependencies:
+    vue-demi "*"
+
+acorn-jsx@^5.3.2:
+  version "5.3.2"
+  resolved "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
+  integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
+
+acorn@^8.8.0, acorn@^8.8.2:
+  version "8.8.2"
+  resolved "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
+  integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
+
+ajv@^6.10.0, ajv@^6.12.4:
+  version "6.12.6"
+  resolved "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+ansi-regex@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^4.1.0:
+  version "4.3.0"
+  resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+any-promise@^1.0.0:
+  version "1.3.0"
+  resolved "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
+  integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
+
+anymatch@~3.1.2:
+  version "3.1.3"
+  resolved "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+  integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+arg@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
+  integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
+
+argparse@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+async-validator@^4.2.5:
+  version "4.2.5"
+  resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz#c96ea3332a521699d0afaaceed510a54656c6339"
+  integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+autoprefixer@^10.4.14:
+  version "10.4.14"
+  resolved "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d"
+  integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==
+  dependencies:
+    browserslist "^4.21.5"
+    caniuse-lite "^1.0.30001464"
+    fraction.js "^4.2.0"
+    normalize-range "^0.1.2"
+    picocolors "^1.0.0"
+    postcss-value-parser "^4.2.0"
+
+axios@^1.3.6:
+  version "1.3.6"
+  resolved "https://registry.npmmirror.com/axios/-/axios-1.3.6.tgz#1ace9a9fb994314b5f6327960918406fa92c6646"
+  integrity sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==
+  dependencies:
+    follow-redirects "^1.15.0"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+boolbase@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+brace-expansion@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
+  integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+  dependencies:
+    balanced-match "^1.0.0"
+
+braces@^3.0.2, braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+browserslist@^4.21.5:
+  version "4.21.5"
+  resolved "https://registry.npmmirror.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7"
+  integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==
+  dependencies:
+    caniuse-lite "^1.0.30001449"
+    electron-to-chromium "^1.4.284"
+    node-releases "^2.0.8"
+    update-browserslist-db "^1.0.10"
+
+call-bind@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+  integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+  dependencies:
+    function-bind "^1.1.1"
+    get-intrinsic "^1.0.2"
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase-css@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
+  integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+
+caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464:
+  version "1.0.30001481"
+  resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz#f58a717afe92f9e69d0e35ff64df596bfad93912"
+  integrity sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==
+
+chalk@^4.0.0:
+  version "4.1.2"
+  resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chokidar@^3.5.3:
+  version "3.5.3"
+  resolved "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+clipboard@^2.0.4:
+  version "2.0.11"
+  resolved "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5"
+  integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==
+  dependencies:
+    good-listener "^1.2.2"
+    select "^1.1.2"
+    tiny-emitter "^2.0.0"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@^1.1.4, color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+commander@^4.0.0:
+  version "4.1.1"
+  resolved "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
+  integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+copy-anything@^2.0.1:
+  version "2.0.6"
+  resolved "https://registry.npmmirror.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480"
+  integrity sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==
+  dependencies:
+    is-what "^3.14.1"
+
+cross-spawn@^7.0.2:
+  version "7.0.3"
+  resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+csstype@^2.6.8:
+  version "2.6.21"
+  resolved "https://registry.npmmirror.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e"
+  integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==
+
+date-fns@^2.29.3:
+  version "2.29.3"
+  resolved "https://registry.npmmirror.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
+  integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
+
+dayjs@^1.11.3:
+  version "1.11.7"
+  resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
+  integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
+
+debug@^3.2.6:
+  version "3.2.7"
+  resolved "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+  dependencies:
+    ms "^2.1.1"
+
+debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
+  version "4.3.4"
+  resolved "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
+deep-is@^0.1.3:
+  version "0.1.4"
+  resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
+  integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+delegate@^3.1.2:
+  version "3.2.0"
+  resolved "https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
+  integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
+
+didyoumean@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
+  integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
+
+dlv@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
+  integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
+
+doctrine@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+  dependencies:
+    esutils "^2.0.2"
+
+electron-to-chromium@^1.4.284:
+  version "1.4.369"
+  resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.369.tgz#a98d838cdd79be4471cd04e9b4dffe891d037874"
+  integrity sha512-LfxbHXdA/S+qyoTEA4EbhxGjrxx7WK2h6yb5K2v0UCOufUKX+VZaHbl3svlzZfv9sGseym/g3Ne4DpsgRULmqg==
+
+element-plus@^2.3.3:
+  version "2.3.3"
+  resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.3.tgz#33173bbbe84ada40f4d796fe5043c44781198ea4"
+  integrity sha512-Zy61OXrG6b4FF3h29A9ZOUkaEQXjCuFwNa7DlpB3Vo+42Tw5zBbHe5a4BY7i56TVJG5xTbS9UQyA726J91pDqg==
+  dependencies:
+    "@ctrl/tinycolor" "^3.4.1"
+    "@element-plus/icons-vue" "^2.0.6"
+    "@floating-ui/dom" "^1.0.1"
+    "@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7"
+    "@types/lodash" "^4.14.182"
+    "@types/lodash-es" "^4.17.6"
+    "@vueuse/core" "^9.1.0"
+    async-validator "^4.2.5"
+    dayjs "^1.11.3"
+    escape-html "^1.0.3"
+    lodash "^4.17.21"
+    lodash-es "^4.17.21"
+    lodash-unified "^1.0.2"
+    memoize-one "^6.0.0"
+    normalize-wheel-es "^1.2.0"
+
+engine.io-client@~6.5.2:
+  version "6.5.2"
+  resolved "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.5.2.tgz#8709e22c291d4297ae80318d3c8baeae71f0e002"
+  integrity sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==
+  dependencies:
+    "@socket.io/component-emitter" "~3.1.0"
+    debug "~4.3.1"
+    engine.io-parser "~5.2.1"
+    ws "~8.11.0"
+    xmlhttprequest-ssl "~2.0.0"
+
+engine.io-parser@~5.2.1:
+  version "5.2.1"
+  resolved "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb"
+  integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==
+
+errno@^0.1.1:
+  version "0.1.8"
+  resolved "https://registry.npmmirror.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
+  integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
+  dependencies:
+    prr "~1.0.1"
+
+esbuild@^0.17.5:
+  version "0.17.18"
+  resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.17.18.tgz#f4f8eb6d77384d68cd71c53eb6601c7efe05e746"
+  integrity sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==
+  optionalDependencies:
+    "@esbuild/android-arm" "0.17.18"
+    "@esbuild/android-arm64" "0.17.18"
+    "@esbuild/android-x64" "0.17.18"
+    "@esbuild/darwin-arm64" "0.17.18"
+    "@esbuild/darwin-x64" "0.17.18"
+    "@esbuild/freebsd-arm64" "0.17.18"
+    "@esbuild/freebsd-x64" "0.17.18"
+    "@esbuild/linux-arm" "0.17.18"
+    "@esbuild/linux-arm64" "0.17.18"
+    "@esbuild/linux-ia32" "0.17.18"
+    "@esbuild/linux-loong64" "0.17.18"
+    "@esbuild/linux-mips64el" "0.17.18"
+    "@esbuild/linux-ppc64" "0.17.18"
+    "@esbuild/linux-riscv64" "0.17.18"
+    "@esbuild/linux-s390x" "0.17.18"
+    "@esbuild/linux-x64" "0.17.18"
+    "@esbuild/netbsd-x64" "0.17.18"
+    "@esbuild/openbsd-x64" "0.17.18"
+    "@esbuild/sunos-x64" "0.17.18"
+    "@esbuild/win32-arm64" "0.17.18"
+    "@esbuild/win32-ia32" "0.17.18"
+    "@esbuild/win32-x64" "0.17.18"
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+escape-html@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+  integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
+
+escape-string-regexp@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+escape-string-regexp@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
+  integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
+
+eslint-config-prettier@^8.3.0:
+  version "8.8.0"
+  resolved "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348"
+  integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==
+
+eslint-plugin-prettier@^4.0.0:
+  version "4.2.1"
+  resolved "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
+  integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
+  dependencies:
+    prettier-linter-helpers "^1.0.0"
+
+eslint-plugin-vue@^9.9.0:
+  version "9.11.0"
+  resolved "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.11.0.tgz#99a247455c02181f24d9240d422380fd16dd630c"
+  integrity sha512-bBCJAZnkBV7ATH4Z1E7CvN3nmtS4H7QUU3UBxPdo8WohRU+yHjnQRALpTbxMVcz0e4Mx3IyxIdP5HYODMxK9cQ==
+  dependencies:
+    "@eslint-community/eslint-utils" "^4.3.0"
+    natural-compare "^1.4.0"
+    nth-check "^2.0.1"
+    postcss-selector-parser "^6.0.9"
+    semver "^7.3.5"
+    vue-eslint-parser "^9.0.1"
+    xml-name-validator "^4.0.0"
+
+eslint-scope@^7.1.1, eslint-scope@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
+  integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^5.2.0"
+
+eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc"
+  integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==
+
+eslint@^8.34.0:
+  version "8.39.0"
+  resolved "https://registry.npmmirror.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1"
+  integrity sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==
+  dependencies:
+    "@eslint-community/eslint-utils" "^4.2.0"
+    "@eslint-community/regexpp" "^4.4.0"
+    "@eslint/eslintrc" "^2.0.2"
+    "@eslint/js" "8.39.0"
+    "@humanwhocodes/config-array" "^0.11.8"
+    "@humanwhocodes/module-importer" "^1.0.1"
+    "@nodelib/fs.walk" "^1.2.8"
+    ajv "^6.10.0"
+    chalk "^4.0.0"
+    cross-spawn "^7.0.2"
+    debug "^4.3.2"
+    doctrine "^3.0.0"
+    escape-string-regexp "^4.0.0"
+    eslint-scope "^7.2.0"
+    eslint-visitor-keys "^3.4.0"
+    espree "^9.5.1"
+    esquery "^1.4.2"
+    esutils "^2.0.2"
+    fast-deep-equal "^3.1.3"
+    file-entry-cache "^6.0.1"
+    find-up "^5.0.0"
+    glob-parent "^6.0.2"
+    globals "^13.19.0"
+    grapheme-splitter "^1.0.4"
+    ignore "^5.2.0"
+    import-fresh "^3.0.0"
+    imurmurhash "^0.1.4"
+    is-glob "^4.0.0"
+    is-path-inside "^3.0.3"
+    js-sdsl "^4.1.4"
+    js-yaml "^4.1.0"
+    json-stable-stringify-without-jsonify "^1.0.1"
+    levn "^0.4.1"
+    lodash.merge "^4.6.2"
+    minimatch "^3.1.2"
+    natural-compare "^1.4.0"
+    optionator "^0.9.1"
+    strip-ansi "^6.0.1"
+    strip-json-comments "^3.1.0"
+    text-table "^0.2.0"
+
+espree@^9.3.1, espree@^9.5.1:
+  version "9.5.1"
+  resolved "https://registry.npmmirror.com/espree/-/espree-9.5.1.tgz#4f26a4d5f18905bf4f2e0bd99002aab807e96dd4"
+  integrity sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==
+  dependencies:
+    acorn "^8.8.0"
+    acorn-jsx "^5.3.2"
+    eslint-visitor-keys "^3.4.0"
+
+esquery@^1.4.0, esquery@^1.4.2:
+  version "1.5.0"
+  resolved "https://registry.npmmirror.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
+  integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==
+  dependencies:
+    estraverse "^5.1.0"
+
+esrecurse@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+  dependencies:
+    estraverse "^5.2.0"
+
+estraverse@^5.1.0, estraverse@^5.2.0:
+  version "5.3.0"
+  resolved "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+  integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
+esutils@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-diff@^1.1.2:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
+  integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
+
+fast-glob@^3.2.12:
+  version "3.2.12"
+  resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
+  integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.2"
+    "@nodelib/fs.walk" "^1.2.3"
+    glob-parent "^5.1.2"
+    merge2 "^1.3.0"
+    micromatch "^4.0.4"
+
+fast-json-stable-stringify@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+  integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+
+fastq@^1.6.0:
+  version "1.15.0"
+  resolved "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
+  integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
+  dependencies:
+    reusify "^1.0.4"
+
+file-entry-cache@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
+  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+  dependencies:
+    flat-cache "^3.0.4"
+
+file-saver@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
+  integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+find-up@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+  integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+  dependencies:
+    locate-path "^6.0.0"
+    path-exists "^4.0.0"
+
+flat-cache@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
+  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
+  dependencies:
+    flatted "^3.1.0"
+    rimraf "^3.0.2"
+
+flatted@^3.1.0:
+  version "3.2.7"
+  resolved "https://registry.npmmirror.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
+  integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
+
+follow-redirects@^1.15.0:
+  version "1.15.2"
+  resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
+  integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
+fraction.js@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
+  integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+get-intrinsic@^1.0.2:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
+  integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.3"
+
+glob-parent@^5.1.2, glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob-parent@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
+  integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+  dependencies:
+    is-glob "^4.0.3"
+
+glob@7.1.6:
+  version "7.1.6"
+  resolved "https://registry.npmmirror.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+glob@^7.1.3:
+  version "7.2.3"
+  resolved "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+  integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.1.1"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+globals@^13.19.0:
+  version "13.20.0"
+  resolved "https://registry.npmmirror.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82"
+  integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==
+  dependencies:
+    type-fest "^0.20.2"
+
+good-listener@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.npmmirror.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
+  integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==
+  dependencies:
+    delegate "^3.1.2"
+
+graceful-fs@^4.1.2:
+  version "4.2.11"
+  resolved "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+  integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+grapheme-splitter@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.npmmirror.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
+  integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-symbols@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+  integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+iconv-lite@^0.6.3:
+  version "0.6.3"
+  resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+  integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3.0.0"
+
+ignore@^5.2.0:
+  version "5.2.4"
+  resolved "https://registry.npmmirror.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
+  integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
+
+image-size@~0.5.0:
+  version "0.5.5"
+  resolved "https://registry.npmmirror.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
+  integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==
+
+import-fresh@^3.0.0, import-fresh@^3.2.1:
+  version "3.3.0"
+  resolved "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
+imurmurhash@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+  integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2:
+  version "2.0.4"
+  resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-core-module@^2.11.0:
+  version "2.12.0"
+  resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.12.0.tgz#36ad62f6f73c8253fd6472517a12483cf03e7ec4"
+  integrity sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==
+  dependencies:
+    has "^1.0.3"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-path-inside@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
+  integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
+
+is-what@^3.14.1:
+  version "3.14.1"
+  resolved "https://registry.npmmirror.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
+  integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+jiti@^1.17.2:
+  version "1.18.2"
+  resolved "https://registry.npmmirror.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd"
+  integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==
+
+js-sdsl@^4.1.4:
+  version "4.4.0"
+  resolved "https://registry.npmmirror.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430"
+  integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==
+
+js-yaml@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+  dependencies:
+    argparse "^2.0.1"
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-stable-stringify-without-jsonify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+  integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
+
+jsonc-parser@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76"
+  integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==
+
+less@^4.1.3:
+  version "4.1.3"
+  resolved "https://registry.npmmirror.com/less/-/less-4.1.3.tgz#175be9ddcbf9b250173e0a00b4d6920a5b770246"
+  integrity sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==
+  dependencies:
+    copy-anything "^2.0.1"
+    parse-node-version "^1.0.1"
+    tslib "^2.3.0"
+  optionalDependencies:
+    errno "^0.1.1"
+    graceful-fs "^4.1.2"
+    image-size "~0.5.0"
+    make-dir "^2.1.0"
+    mime "^1.4.1"
+    needle "^3.1.0"
+    source-map "~0.6.0"
+
+levn@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
+  integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
+  dependencies:
+    prelude-ls "^1.2.1"
+    type-check "~0.4.0"
+
+lilconfig@^2.0.5, lilconfig@^2.0.6:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
+  integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
+
+lines-and-columns@^1.1.6:
+  version "1.2.4"
+  resolved "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
+  integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+
+local-pkg@^0.4.3:
+  version "0.4.3"
+  resolved "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963"
+  integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==
+
+locate-path@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+  integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+  dependencies:
+    p-locate "^5.0.0"
+
+lodash-es@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
+lodash-unified@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz#80b1eac10ed2eb02ed189f08614a29c27d07c894"
+  integrity sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==
+
+lodash.merge@^4.6.2:
+  version "4.6.2"
+  resolved "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
+lodash@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
+magic-string@^0.25.7:
+  version "0.25.9"
+  resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
+  integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
+  dependencies:
+    sourcemap-codec "^1.4.8"
+
+magic-string@^0.30.0:
+  version "0.30.0"
+  resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529"
+  integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==
+  dependencies:
+    "@jridgewell/sourcemap-codec" "^1.4.13"
+
+make-dir@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
+  integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
+  dependencies:
+    pify "^4.0.1"
+    semver "^5.6.0"
+
+memoize-one@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
+  integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
+
+merge2@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4, micromatch@^4.0.5:
+  version "4.0.5"
+  resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+  dependencies:
+    braces "^3.0.2"
+    picomatch "^2.3.1"
+
+mime-db@1.52.0:
+  version "1.52.0"
+  resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+  version "2.1.35"
+  resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+  dependencies:
+    mime-db "1.52.0"
+
+mime@^1.4.1:
+  version "1.6.0"
+  resolved "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimatch@^7.4.2:
+  version "7.4.6"
+  resolved "https://registry.npmmirror.com/minimatch/-/minimatch-7.4.6.tgz#845d6f254d8f4a5e4fd6baf44d5f10c8448365fb"
+  integrity sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==
+  dependencies:
+    brace-expansion "^2.0.1"
+
+minimatch@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.0.tgz#bfc8e88a1c40ffd40c172ddac3decb8451503b56"
+  integrity sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==
+  dependencies:
+    brace-expansion "^2.0.1"
+
+mlly@^1.1.1, mlly@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/mlly/-/mlly-1.2.0.tgz#f0f6c2fc8d2d12ea6907cd869066689b5031b613"
+  integrity sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==
+  dependencies:
+    acorn "^8.8.2"
+    pathe "^1.1.0"
+    pkg-types "^1.0.2"
+    ufo "^1.1.1"
+
+moment-timezone@^0.5.45:
+  version "0.5.45"
+  resolved "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.45.tgz#cb685acd56bac10e69d93c536366eb65aa6bcf5c"
+  integrity sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==
+  dependencies:
+    moment "^2.29.4"
+
+moment@^2.29.4, moment@^2.30.1:
+  version "2.30.1"
+  resolved "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
+  integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
+
+ms@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+mz@^2.7.0:
+  version "2.7.0"
+  resolved "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
+  integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
+  dependencies:
+    any-promise "^1.0.0"
+    object-assign "^4.0.1"
+    thenify-all "^1.0.0"
+
+nanoid@^3.3.6:
+  version "3.3.6"
+  resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
+  integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
+
+natural-compare@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+  integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
+
+needle@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.npmmirror.com/needle/-/needle-3.2.0.tgz#07d240ebcabfd65c76c03afae7f6defe6469df44"
+  integrity sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==
+  dependencies:
+    debug "^3.2.6"
+    iconv-lite "^0.6.3"
+    sax "^1.2.4"
+
+node-releases@^2.0.8:
+  version "2.0.10"
+  resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
+  integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-range@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+  integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
+
+normalize-wheel-es@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz#0fa2593d619f7245a541652619105ab076acf09e"
+  integrity sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==
+
+nth-check@^2.0.1:
+  version "2.1.1"
+  resolved "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
+  integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
+  dependencies:
+    boolbase "^1.0.0"
+
+object-assign@^4.0.1:
+  version "4.1.1"
+  resolved "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
+object-hash@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
+  integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
+
+object-inspect@^1.9.0:
+  version "1.12.3"
+  resolved "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
+  integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
+
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.npmmirror.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+  dependencies:
+    wrappy "1"
+
+optionator@^0.9.1:
+  version "0.9.1"
+  resolved "https://registry.npmmirror.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
+  integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
+  dependencies:
+    deep-is "^0.1.3"
+    fast-levenshtein "^2.0.6"
+    levn "^0.4.1"
+    prelude-ls "^1.2.1"
+    type-check "^0.4.0"
+    word-wrap "^1.2.3"
+
+p-limit@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+  dependencies:
+    yocto-queue "^0.1.0"
+
+p-locate@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+  integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+  dependencies:
+    p-limit "^3.0.2"
+
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+  dependencies:
+    callsites "^3.0.0"
+
+parse-node-version@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
+  integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+pathe@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmmirror.com/pathe/-/pathe-1.1.0.tgz#e2e13f6c62b31a3289af4ba19886c230f295ec03"
+  integrity sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pify@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+  integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
+
+pify@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.npmmirror.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+  integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+pinia@^2.0.32:
+  version "2.0.35"
+  resolved "https://registry.npmmirror.com/pinia/-/pinia-2.0.35.tgz#aa2597038bb55ea14ad689f83065d2814ebb8c10"
+  integrity sha512-P1IKKQWhxGXiiZ3atOaNI75bYlFUbRxtJdhPLX059Z7+b9Z04rnTZdSY8Aph1LA+/4QEMAYHsTQ638Wfe+6K5g==
+  dependencies:
+    "@vue/devtools-api" "^6.5.0"
+    vue-demi "*"
+
+pirates@^4.0.1:
+  version "4.0.5"
+  resolved "https://registry.npmmirror.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
+  integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
+
+pkg-types@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.0.2.tgz#c233efc5210a781e160e0cafd60c0d0510a4b12e"
+  integrity sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==
+  dependencies:
+    jsonc-parser "^3.2.0"
+    mlly "^1.1.1"
+    pathe "^1.1.0"
+
+postcss-import@^14.1.0:
+  version "14.1.0"
+  resolved "https://registry.npmmirror.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"
+  integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==
+  dependencies:
+    postcss-value-parser "^4.0.0"
+    read-cache "^1.0.0"
+    resolve "^1.1.7"
+
+postcss-js@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2"
+  integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
+  dependencies:
+    camelcase-css "^2.0.1"
+
+postcss-load-config@^3.1.4:
+  version "3.1.4"
+  resolved "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
+  integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
+  dependencies:
+    lilconfig "^2.0.5"
+    yaml "^1.10.2"
+
+postcss-nested@6.0.0:
+  version "6.0.0"
+  resolved "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.0.0.tgz#1572f1984736578f360cffc7eb7dca69e30d1735"
+  integrity sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==
+  dependencies:
+    postcss-selector-parser "^6.0.10"
+
+postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.9:
+  version "6.0.11"
+  resolved "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc"
+  integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+  integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^8.0.9, postcss@^8.1.10, postcss@^8.4.21, postcss@^8.4.23:
+  version "8.4.23"
+  resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab"
+  integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==
+  dependencies:
+    nanoid "^3.3.6"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
+prelude-ls@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
+  integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+
+prettier-linter-helpers@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
+  integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
+  dependencies:
+    fast-diff "^1.1.2"
+
+prettier@^2.8.4:
+  version "2.8.8"
+  resolved "https://registry.npmmirror.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
+  integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
+
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
+prr@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+  integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==
+
+punycode@^2.1.0:
+  version "2.3.0"
+  resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
+  integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+
+qs@^6.11.1:
+  version "6.11.1"
+  resolved "https://registry.npmmirror.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f"
+  integrity sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==
+  dependencies:
+    side-channel "^1.0.4"
+
+queue-microtask@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+quick-lru@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.npmmirror.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
+  integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
+
+read-cache@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
+  integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
+  dependencies:
+    pify "^2.3.0"
+
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve-url@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.npmmirror.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+  integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==
+
+resolve@^1.1.7, resolve@^1.22.1:
+  version "1.22.2"
+  resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f"
+  integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==
+  dependencies:
+    is-core-module "^2.11.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rimraf@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+  dependencies:
+    glob "^7.1.3"
+
+rollup@^3.20.2:
+  version "3.20.7"
+  resolved "https://registry.npmmirror.com/rollup/-/rollup-3.20.7.tgz#4f045dfb388abe08dd159f8cd286dcaca1e80b28"
+  integrity sha512-P7E2zezKSLhWnTz46XxjSmInrbOCiul1yf+kJccMxT56vxjHwCbDfoLbiqFgu+WQoo9ij2PkraYaBstgB2prBA==
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+run-parallel@^1.1.9:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+  integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+  dependencies:
+    queue-microtask "^1.2.2"
+
+"safer-buffer@>= 2.1.2 < 3.0.0":
+  version "2.1.2"
+  resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sax@^1.2.4:
+  version "1.2.4"
+  resolved "https://registry.npmmirror.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+  integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+scule@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/scule/-/scule-1.0.0.tgz#895e6f4ba887e78d8b9b4111e23ae84fef82376d"
+  integrity sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==
+
+select@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.npmmirror.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
+  integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==
+
+semver@^5.6.0:
+  version "5.7.1"
+  resolved "https://registry.npmmirror.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@^7.3.5, semver@^7.3.6:
+  version "7.5.0"
+  resolved "https://registry.npmmirror.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0"
+  integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==
+  dependencies:
+    lru-cache "^6.0.0"
+
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+  dependencies:
+    shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+side-channel@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
+  integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
+  dependencies:
+    call-bind "^1.0.0"
+    get-intrinsic "^1.0.2"
+    object-inspect "^1.9.0"
+
+socket.io-client@^4.7.2:
+  version "4.7.2"
+  resolved "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08"
+  integrity sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==
+  dependencies:
+    "@socket.io/component-emitter" "~3.1.0"
+    debug "~4.3.2"
+    engine.io-client "~6.5.2"
+    socket.io-parser "~4.2.4"
+
+socket.io-parser@~4.2.4:
+  version "4.2.4"
+  resolved "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83"
+  integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==
+  dependencies:
+    "@socket.io/component-emitter" "~3.1.0"
+    debug "~4.3.1"
+
+source-map-js@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
+source-map@^0.6.1, source-map@~0.6.0:
+  version "0.6.1"
+  resolved "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+sourcemap-codec@^1.4.8:
+  version "1.4.8"
+  resolved "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
+  integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
+
+strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
+strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+strip-literal@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/strip-literal/-/strip-literal-1.0.1.tgz#0115a332710c849b4e46497891fb8d585e404bd2"
+  integrity sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==
+  dependencies:
+    acorn "^8.8.2"
+
+sucrase@^3.29.0:
+  version "3.32.0"
+  resolved "https://registry.npmmirror.com/sucrase/-/sucrase-3.32.0.tgz#c4a95e0f1e18b6847127258a75cf360bc568d4a7"
+  integrity sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.3.2"
+    commander "^4.0.0"
+    glob "7.1.6"
+    lines-and-columns "^1.1.6"
+    mz "^2.7.0"
+    pirates "^4.0.1"
+    ts-interface-checker "^0.1.9"
+
+supports-color@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+tailwindcss@^3.3.1:
+  version "3.3.1"
+  resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.1.tgz#b6662fab6a9b704779e48d083a9fef5a81d2b81e"
+  integrity sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==
+  dependencies:
+    arg "^5.0.2"
+    chokidar "^3.5.3"
+    color-name "^1.1.4"
+    didyoumean "^1.2.2"
+    dlv "^1.1.3"
+    fast-glob "^3.2.12"
+    glob-parent "^6.0.2"
+    is-glob "^4.0.3"
+    jiti "^1.17.2"
+    lilconfig "^2.0.6"
+    micromatch "^4.0.5"
+    normalize-path "^3.0.0"
+    object-hash "^3.0.0"
+    picocolors "^1.0.0"
+    postcss "^8.0.9"
+    postcss-import "^14.1.0"
+    postcss-js "^4.0.0"
+    postcss-load-config "^3.1.4"
+    postcss-nested "6.0.0"
+    postcss-selector-parser "^6.0.11"
+    postcss-value-parser "^4.2.0"
+    quick-lru "^5.1.1"
+    resolve "^1.22.1"
+    sucrase "^3.29.0"
+
+text-table@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+  integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
+
+thenify-all@^1.0.0:
+  version "1.6.0"
+  resolved "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
+  integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
+  dependencies:
+    thenify ">= 3.1.0 < 4"
+
+"thenify@>= 3.1.0 < 4":
+  version "3.3.1"
+  resolved "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
+  integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
+  dependencies:
+    any-promise "^1.0.0"
+
+tiny-emitter@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
+  integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+ts-interface-checker@^0.1.9:
+  version "0.1.13"
+  resolved "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
+  integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
+
+tslib@^2.3.0:
+  version "2.5.0"
+  resolved "https://registry.npmmirror.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
+  integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
+
+type-check@^0.4.0, type-check@~0.4.0:
+  version "0.4.0"
+  resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
+  integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
+  dependencies:
+    prelude-ls "^1.2.1"
+
+type-fest@^0.20.2:
+  version "0.20.2"
+  resolved "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+  integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
+
+ufo@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/ufo/-/ufo-1.1.1.tgz#e70265e7152f3aba425bd013d150b2cdf4056d7c"
+  integrity sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==
+
+unimport@^3.0.6:
+  version "3.0.6"
+  resolved "https://registry.npmmirror.com/unimport/-/unimport-3.0.6.tgz#20caac4cf9a94e9233cdbe3a16f7599ae75a7df8"
+  integrity sha512-GYxGJ1Bri1oqx8VFDjdgooGzeK7jBk3bvhXmamTIpu3nONOcUMGwZbX7X0L5RA7OWMXpR4vzpSQP7pXUzJg1/Q==
+  dependencies:
+    "@rollup/pluginutils" "^5.0.2"
+    escape-string-regexp "^5.0.0"
+    fast-glob "^3.2.12"
+    local-pkg "^0.4.3"
+    magic-string "^0.30.0"
+    mlly "^1.2.0"
+    pathe "^1.1.0"
+    pkg-types "^1.0.2"
+    scule "^1.0.0"
+    strip-literal "^1.0.1"
+    unplugin "^1.3.1"
+
+unplugin-auto-import@^0.15.3:
+  version "0.15.3"
+  resolved "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.15.3.tgz#abf5f1bf42f8c181e9dd1067bd5645aad727df6e"
+  integrity sha512-RLT8SqbPn4bT7yBshZId0uPSofKWnwr66RyDaxWaFb/+f7OTDOWAsVNz+hOQLBWSjvbekr2xZY9ccS8TDHJbCQ==
+  dependencies:
+    "@antfu/utils" "^0.7.2"
+    "@rollup/pluginutils" "^5.0.2"
+    local-pkg "^0.4.3"
+    magic-string "^0.30.0"
+    minimatch "^9.0.0"
+    unimport "^3.0.6"
+    unplugin "^1.3.1"
+
+unplugin-vue-components@^0.24.1:
+  version "0.24.1"
+  resolved "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.24.1.tgz#b5c3419c30a603dd795e3a0d63c4c12f4a5d8274"
+  integrity sha512-T3A8HkZoIE1Cja95xNqolwza0yD5IVlgZZ1PVAGvVCx8xthmjsv38xWRCtHtwl+rvZyL9uif42SRkDGw9aCfMA==
+  dependencies:
+    "@antfu/utils" "^0.7.2"
+    "@rollup/pluginutils" "^5.0.2"
+    chokidar "^3.5.3"
+    debug "^4.3.4"
+    fast-glob "^3.2.12"
+    local-pkg "^0.4.3"
+    magic-string "^0.30.0"
+    minimatch "^7.4.2"
+    resolve "^1.22.1"
+    unplugin "^1.1.0"
+
+unplugin@^1.1.0, unplugin@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.npmmirror.com/unplugin/-/unplugin-1.3.1.tgz#7af993ba8695d17d61b0845718380caf6af5109f"
+  integrity sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==
+  dependencies:
+    acorn "^8.8.2"
+    chokidar "^3.5.3"
+    webpack-sources "^3.2.3"
+    webpack-virtual-modules "^0.5.0"
+
+update-browserslist-db@^1.0.10:
+  version "1.0.11"
+  resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
+  integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==
+  dependencies:
+    escalade "^3.1.1"
+    picocolors "^1.0.0"
+
+uri-js@^4.2.2:
+  version "4.4.1"
+  resolved "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+  dependencies:
+    punycode "^2.1.0"
+
+util-deprecate@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+vite@^4.1.4:
+  version "4.3.1"
+  resolved "https://registry.npmmirror.com/vite/-/vite-4.3.1.tgz#9badb1377f995632cdcf05f32103414db6fbb95a"
+  integrity sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==
+  dependencies:
+    esbuild "^0.17.5"
+    postcss "^8.4.21"
+    rollup "^3.20.2"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+vue-demi@*, vue-demi@>=0.14.0:
+  version "0.14.0"
+  resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.0.tgz#dcfd9a9cf9bb62ada1582ec9042372cf67ca6190"
+  integrity sha512-gt58r2ogsNQeVoQ3EhoUAvUsH9xviydl0dWJj7dabBC/2L4uBId7ujtCwDRD0JhkGsV1i0CtfLAeyYKBht9oWg==
+
+vue-eslint-parser@^9.0.1:
+  version "9.1.1"
+  resolved "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.1.1.tgz#3f4859be7e9bb7edaa1dc7edb05abffee72bf3dd"
+  integrity sha512-C2aI/r85Q6tYcz4dpgvrs4wH/MqVrRAVIdpYedrxnATDHHkb+TroeRcDpKWGZCx/OcECMWfz7tVwQ8e+Opy6rA==
+  dependencies:
+    debug "^4.3.4"
+    eslint-scope "^7.1.1"
+    eslint-visitor-keys "^3.3.0"
+    espree "^9.3.1"
+    esquery "^1.4.0"
+    lodash "^4.17.21"
+    semver "^7.3.6"
+
+vue-json-viewer@3:
+  version "3.0.4"
+  resolved "https://registry.npmmirror.com/vue-json-viewer/-/vue-json-viewer-3.0.4.tgz#c1d65515e57d4036defbbc18fa942d7fd5fb9a8b"
+  integrity sha512-pnC080rTub6YjccthVSNQod2z9Sl5IUUq46srXtn6rxwhW8QM4rlYn+CTSLFKXWfw+N3xv77Cioxw7B4XUKIbQ==
+  dependencies:
+    clipboard "^2.0.4"
+
+vue-router@^4.1.6:
+  version "4.1.6"
+  resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.6.tgz#b70303737e12b4814578d21d68d21618469375a1"
+  integrity sha512-DYWYwsG6xNPmLq/FmZn8Ip+qrhFEzA14EI12MsMgVxvHFDYvlr4NXpVF5hrRH1wVcDP8fGi5F4rxuJSl8/r+EQ==
+  dependencies:
+    "@vue/devtools-api" "^6.4.5"
+
+vue@^3.2.47:
+  version "3.2.47"
+  resolved "https://registry.npmmirror.com/vue/-/vue-3.2.47.tgz#3eb736cbc606fc87038dbba6a154707c8a34cff0"
+  integrity sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==
+  dependencies:
+    "@vue/compiler-dom" "3.2.47"
+    "@vue/compiler-sfc" "3.2.47"
+    "@vue/runtime-dom" "3.2.47"
+    "@vue/server-renderer" "3.2.47"
+    "@vue/shared" "3.2.47"
+
+webpack-sources@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
+  integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
+
+webpack-virtual-modules@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c"
+  integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==
+
+which@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.npmmirror.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+  dependencies:
+    isexe "^2.0.0"
+
+word-wrap@^1.2.3:
+  version "1.2.3"
+  resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+ws@~8.11.0:
+  version "8.11.0"
+  resolved "https://registry.npmmirror.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
+  integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
+
+xml-name-validator@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
+  integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
+
+xmlhttprequest-ssl@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
+  integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yaml@^1.10.2:
+  version "1.10.2"
+  resolved "https://registry.npmmirror.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+yocto-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==

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