liyongli 4 місяців тому
коміт
4bbda90fa0
48 змінених файлів з 6573 додано та 0 видалено
  1. 6 0
      .editorconfig
  2. 30 0
      .gitignore
  3. 7 0
      .prettierrc.json
  4. 8 0
      .vscode/extensions.json
  5. 39 0
      README.md
  6. 10 0
      auto-imports.d.ts
  7. 63 0
      components.d.ts
  8. 1 0
      env.d.ts
  9. 19 0
      eslint.config.js
  10. 16 0
      index.html
  11. 39 0
      package.json
  12. 2670 0
      pnpm-lock.yaml
  13. BIN
      public/favicon.ico
  14. 9 0
      src/App.vue
  15. 269 0
      src/api/index.ts
  16. 86 0
      src/assets/base.css
  17. 29 0
      src/assets/main.css
  18. 191 0
      src/components/mediaComponents.vue
  19. 24 0
      src/main.ts
  20. 80 0
      src/router/index.ts
  21. 12 0
      src/stores/counter.ts
  22. 138 0
      src/tool/adList.ts
  23. 61 0
      src/tool/axios.ts
  24. 64 0
      src/tool/index.ts
  25. 84 0
      src/types/AdListPage.d.ts
  26. 17 0
      src/types/Advertiser.d.ts
  27. 68 0
      src/types/AsSpace.d.ts
  28. 12 0
      src/types/Catalog.d.ts
  29. 7 0
      src/types/Report.d.ts
  30. 18 0
      src/types/Site.d.ts
  31. 31 0
      src/types/SourceMaterial.d.ts
  32. 53 0
      src/types/Tool.d.ts
  33. 5 0
      src/types/User.d.ts
  34. 99 0
      src/views/AdListPage.vue
  35. 195 0
      src/views/AdvertiserPage.vue
  36. 195 0
      src/views/AdvertisingSpaceDatePage.vue
  37. 266 0
      src/views/AdvertisingSpacePage.vue
  38. 142 0
      src/views/LoginPage.vue
  39. 426 0
      src/views/OriginalityPage.vue
  40. 226 0
      src/views/ReportFormsPage.vue
  41. 225 0
      src/views/ReportSpaceFormsPage.vue
  42. 191 0
      src/views/SitePage.vue
  43. 85 0
      src/views/SkeletonPage.vue
  44. 285 0
      src/views/SourceMaterialPage.vue
  45. 14 0
      tsconfig.app.json
  46. 14 0
      tsconfig.json
  47. 19 0
      tsconfig.node.json
  48. 25 0
      vite.config.ts

+ 6 - 0
.editorconfig

@@ -0,0 +1,6 @@
+[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
+charset = utf-8
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 30 - 0
.gitignore

@@ -0,0 +1,30 @@
+# 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?
+
+*.tsbuildinfo

+ 7 - 0
.prettierrc.json

@@ -0,0 +1,7 @@
+
+{
+  "$schema": "https://json.schemastore.org/prettierrc",
+  "semi": false,
+  "singleQuote": true,
+  "printWidth": 100
+}

+ 8 - 0
.vscode/extensions.json

@@ -0,0 +1,8 @@
+{
+  "recommendations": [
+    "Vue.volar",
+    "dbaeumer.vscode-eslint",
+    "EditorConfig.EditorConfig",
+    "esbenp.prettier-vscode"
+  ]
+}

+ 39 - 0
README.md

@@ -0,0 +1,39 @@
+# ad
+
+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).
+
+## Type Support for `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vite.dev/config/).
+
+## Project Setup
+
+```sh
+pnpm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+pnpm dev
+```
+
+### Type-Check, Compile and Minify for Production
+
+```sh
+pnpm build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+pnpm lint
+```

+ 10 - 0
auto-imports.d.ts

@@ -0,0 +1,10 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+// biome-ignore lint: disable
+export {}
+declare global {
+  const ElMessage: typeof import('element-plus/es')['ElMessage']
+}

+ 63 - 0
components.d.ts

@@ -0,0 +1,63 @@
+/* eslint-disable */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+export {}
+
+/* prettier-ignore */
+declare module 'vue' {
+  export interface GlobalComponents {
+    ElAside: typeof import('element-plus/es')['ElAside']
+    ElButton: typeof import('element-plus/es')['ElButton']
+    ElCascader: typeof import('element-plus/es')['ElCascader']
+    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
+    ElCol: typeof import('element-plus/es')['ElCol']
+    ElContainer: typeof import('element-plus/es')['ElContainer']
+    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
+    ElDialog: typeof import('element-plus/es')['ElDialog']
+    ElDropdown: typeof import('element-plus/es')['ElDropdown']
+    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
+    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
+    ElEmpty: typeof import('element-plus/es')['ElEmpty']
+    ElFooter: typeof import('element-plus/es')['ElFooter']
+    ElForm: typeof import('element-plus/es')['ElForm']
+    ElFormItem: typeof import('element-plus/es')['ElFormItem']
+    ElHeader: typeof import('element-plus/es')['ElHeader']
+    ElIcon: typeof import('element-plus/es')['ElIcon']
+    ElImage: typeof import('element-plus/es')['ElImage']
+    ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
+    ElMain: typeof import('element-plus/es')['ElMain']
+    ElMenu: typeof import('element-plus/es')['ElMenu']
+    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+    ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
+    ElOption: typeof import('element-plus/es')['ElOption']
+    ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
+    ElRadio: typeof import('element-plus/es')['ElRadio']
+    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
+    ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElStep: typeof import('element-plus/es')['ElStep']
+    ElSteps: typeof import('element-plus/es')['ElSteps']
+    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
+    ElSwitch: typeof import('element-plus/es')['ElSwitch']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTabPane: typeof import('element-plus/es')['ElTabPane']
+    ElTabs: typeof import('element-plus/es')['ElTabs']
+    ElTag: typeof import('element-plus/es')['ElTag']
+    ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
+    ElTooltip: typeof import('element-plus/es')['ElTooltip']
+    ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
+    ElUpload: typeof import('element-plus/es')['ElUpload']
+    Media: typeof import('./src/components/media.vue')['default']
+    MediaComponents: typeof import('./src/components/mediaComponents.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+  }
+  export interface ComponentCustomProperties {
+    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
+  }
+}

+ 1 - 0
env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 19 - 0
eslint.config.js

@@ -0,0 +1,19 @@
+import pluginVue from 'eslint-plugin-vue'
+import vueTsEslintConfig from '@vue/eslint-config-typescript'
+import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
+
+export default [
+  {
+    name: 'app/files-to-lint',
+    files: ['**/*.{ts,mts,tsx,vue}'],
+  },
+
+  {
+    name: 'app/files-to-ignore',
+    ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
+  },
+
+  ...pluginVue.configs['flat/essential'],
+  ...vueTsEslintConfig(),
+  skipFormatting,
+]

+ 16 - 0
index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="">
+
+<head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>广告系统</title>
+</head>
+
+<body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+</body>
+
+</html>

+ 39 - 0
package.json

@@ -0,0 +1,39 @@
+{
+  "name": "ad",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "run-p type-check \"build-only {@}\" --",
+    "preview": "vite preview",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --build --force",
+    "lint": "eslint . --fix",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "axios": "^1.7.7",
+    "element-plus": "^2.8.7",
+    "pinia": "^2.2.6",
+    "vue": "^3.5.12",
+    "vue-router": "^4.4.5"
+  },
+  "devDependencies": {
+    "@tsconfig/node20": "^20.1.4",
+    "@types/node": "^20.17.6",
+    "@vitejs/plugin-vue": "^5.1.4",
+    "@vue/eslint-config-prettier": "^10.1.0",
+    "@vue/eslint-config-typescript": "^14.1.3",
+    "@vue/tsconfig": "^0.5.1",
+    "eslint": "^9.14.0",
+    "eslint-plugin-vue": "^9.30.0",
+    "npm-run-all2": "^7.0.1",
+    "prettier": "^3.3.3",
+    "typescript": "~5.6.3",
+    "unplugin-auto-import": "^0.18.3",
+    "unplugin-vue-components": "^0.27.4",
+    "vite": "^5.4.10",
+    "vue-tsc": "^2.1.10"
+  }
+}

+ 2670 - 0
pnpm-lock.yaml

@@ -0,0 +1,2670 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    dependencies:
+      axios:
+        specifier: ^1.7.7
+        version: 1.7.7
+      element-plus:
+        specifier: ^2.8.7
+        version: 2.8.7(vue@3.5.12(typescript@5.6.3))
+      pinia:
+        specifier: ^2.2.6
+        version: 2.2.6(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))
+      vue:
+        specifier: ^3.5.12
+        version: 3.5.12(typescript@5.6.3)
+      vue-router:
+        specifier: ^4.4.5
+        version: 4.4.5(vue@3.5.12(typescript@5.6.3))
+    devDependencies:
+      '@tsconfig/node20':
+        specifier: ^20.1.4
+        version: 20.1.4
+      '@types/node':
+        specifier: ^20.17.6
+        version: 20.17.6
+      '@vitejs/plugin-vue':
+        specifier: ^5.1.4
+        version: 5.1.4(vite@5.4.10(@types/node@20.17.6))(vue@3.5.12(typescript@5.6.3))
+      '@vue/eslint-config-prettier':
+        specifier: ^10.1.0
+        version: 10.1.0(eslint@9.14.0)(prettier@3.3.3)
+      '@vue/eslint-config-typescript':
+        specifier: ^14.1.3
+        version: 14.1.3(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint-plugin-vue@9.30.0(eslint@9.14.0))(eslint@9.14.0)(typescript@5.6.3)
+      '@vue/tsconfig':
+        specifier: ^0.5.1
+        version: 0.5.1
+      eslint:
+        specifier: ^9.14.0
+        version: 9.14.0
+      eslint-plugin-vue:
+        specifier: ^9.30.0
+        version: 9.30.0(eslint@9.14.0)
+      npm-run-all2:
+        specifier: ^7.0.1
+        version: 7.0.1
+      prettier:
+        specifier: ^3.3.3
+        version: 3.3.3
+      typescript:
+        specifier: ~5.6.3
+        version: 5.6.3
+      unplugin-auto-import:
+        specifier: ^0.18.3
+        version: 0.18.3(@vueuse/core@9.13.0(vue@3.5.12(typescript@5.6.3)))(rollup@4.24.4)
+      unplugin-vue-components:
+        specifier: ^0.27.4
+        version: 0.27.4(@babel/parser@7.26.2)(rollup@4.24.4)(vue@3.5.12(typescript@5.6.3))
+      vite:
+        specifier: ^5.4.10
+        version: 5.4.10(@types/node@20.17.6)
+      vue-tsc:
+        specifier: ^2.1.10
+        version: 2.1.10(typescript@5.6.3)
+
+packages:
+
+  '@antfu/utils@0.7.10':
+    resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
+
+  '@babel/helper-string-parser@7.25.9':
+    resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-validator-identifier@7.25.9':
+    resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/parser@7.26.2':
+    resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+
+  '@babel/types@7.26.0':
+    resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==}
+    engines: {node: '>=6.9.0'}
+
+  '@ctrl/tinycolor@3.6.1':
+    resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
+    engines: {node: '>=10'}
+
+  '@element-plus/icons-vue@2.3.1':
+    resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==}
+    peerDependencies:
+      vue: ^3.2.0
+
+  '@esbuild/aix-ppc64@0.21.5':
+    resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/android-arm64@0.21.5':
+    resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm@0.21.5':
+    resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-x64@0.21.5':
+    resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/darwin-arm64@0.21.5':
+    resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.21.5':
+    resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/freebsd-arm64@0.21.5':
+    resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.21.5':
+    resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/linux-arm64@0.21.5':
+    resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.21.5':
+    resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.21.5':
+    resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.21.5':
+    resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
+    engines: {node: '>=12'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.21.5':
+    resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
+    engines: {node: '>=12'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.21.5':
+    resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.21.5':
+    resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
+    engines: {node: '>=12'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.21.5':
+    resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
+    engines: {node: '>=12'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.21.5':
+    resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/netbsd-x64@0.21.5':
+    resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/openbsd-x64@0.21.5':
+    resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/sunos-x64@0.21.5':
+    resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/win32-arm64@0.21.5':
+    resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.21.5':
+    resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.21.5':
+    resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [win32]
+
+  '@eslint-community/eslint-utils@4.4.1':
+    resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+  '@eslint-community/regexpp@4.12.1':
+    resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+    engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+  '@eslint/config-array@0.18.0':
+    resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/core@0.7.0':
+    resolution: {integrity: sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/eslintrc@3.1.0':
+    resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/js@9.14.0':
+    resolution: {integrity: sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/object-schema@2.1.4':
+    resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/plugin-kit@0.2.2':
+    resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@floating-ui/core@1.6.8':
+    resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==}
+
+  '@floating-ui/dom@1.6.12':
+    resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==}
+
+  '@floating-ui/utils@0.2.8':
+    resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
+
+  '@humanfs/core@0.19.1':
+    resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanfs/node@0.16.6':
+    resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanwhocodes/module-importer@1.0.1':
+    resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+    engines: {node: '>=12.22'}
+
+  '@humanwhocodes/retry@0.3.1':
+    resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
+    engines: {node: '>=18.18'}
+
+  '@humanwhocodes/retry@0.4.1':
+    resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==}
+    engines: {node: '>=18.18'}
+
+  '@jridgewell/sourcemap-codec@1.5.0':
+    resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+
+  '@nodelib/fs.scandir@2.1.5':
+    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+    engines: {node: '>= 8'}
+
+  '@nodelib/fs.stat@2.0.5':
+    resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+    engines: {node: '>= 8'}
+
+  '@nodelib/fs.walk@1.2.8':
+    resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+    engines: {node: '>= 8'}
+
+  '@pkgr/core@0.1.1':
+    resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
+    engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+
+  '@rollup/pluginutils@5.1.3':
+    resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==}
+    engines: {node: '>=14.0.0'}
+    peerDependencies:
+      rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+    peerDependenciesMeta:
+      rollup:
+        optional: true
+
+  '@rollup/rollup-android-arm-eabi@4.24.4':
+    resolution: {integrity: sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw==}
+    cpu: [arm]
+    os: [android]
+
+  '@rollup/rollup-android-arm64@4.24.4':
+    resolution: {integrity: sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA==}
+    cpu: [arm64]
+    os: [android]
+
+  '@rollup/rollup-darwin-arm64@4.24.4':
+    resolution: {integrity: sha512-GmU/QgGtBTeraKyldC7cDVVvAJEOr3dFLKneez/n7BvX57UdhOqDsVwzU7UOnYA7AAOt+Xb26lk79PldDHgMIQ==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@rollup/rollup-darwin-x64@4.24.4':
+    resolution: {integrity: sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@rollup/rollup-freebsd-arm64@4.24.4':
+    resolution: {integrity: sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw==}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@rollup/rollup-freebsd-x64@4.24.4':
+    resolution: {integrity: sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g==}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.24.4':
+    resolution: {integrity: sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA==}
+    cpu: [arm]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-arm-musleabihf@4.24.4':
+    resolution: {integrity: sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw==}
+    cpu: [arm]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-arm64-gnu@4.24.4':
+    resolution: {integrity: sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-arm64-musl@4.24.4':
+    resolution: {integrity: sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-powerpc64le-gnu@4.24.4':
+    resolution: {integrity: sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA==}
+    cpu: [ppc64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-riscv64-gnu@4.24.4':
+    resolution: {integrity: sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw==}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-s390x-gnu@4.24.4':
+    resolution: {integrity: sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ==}
+    cpu: [s390x]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-x64-gnu@4.24.4':
+    resolution: {integrity: sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-x64-musl@4.24.4':
+    resolution: {integrity: sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q==}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-win32-arm64-msvc@4.24.4':
+    resolution: {integrity: sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA==}
+    cpu: [arm64]
+    os: [win32]
+
+  '@rollup/rollup-win32-ia32-msvc@4.24.4':
+    resolution: {integrity: sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg==}
+    cpu: [ia32]
+    os: [win32]
+
+  '@rollup/rollup-win32-x64-msvc@4.24.4':
+    resolution: {integrity: sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg==}
+    cpu: [x64]
+    os: [win32]
+
+  '@sxzz/popperjs-es@2.11.7':
+    resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
+
+  '@tsconfig/node20@20.1.4':
+    resolution: {integrity: sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==}
+
+  '@types/estree@1.0.6':
+    resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
+
+  '@types/json-schema@7.0.15':
+    resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+  '@types/lodash-es@4.17.12':
+    resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
+
+  '@types/lodash@4.17.13':
+    resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==}
+
+  '@types/node@20.17.6':
+    resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==}
+
+  '@types/web-bluetooth@0.0.16':
+    resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
+
+  '@typescript-eslint/eslint-plugin@8.13.0':
+    resolution: {integrity: sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/parser@8.13.0':
+    resolution: {integrity: sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/scope-manager@8.13.0':
+    resolution: {integrity: sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/type-utils@8.13.0':
+    resolution: {integrity: sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/types@8.13.0':
+    resolution: {integrity: sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/typescript-estree@8.13.0':
+    resolution: {integrity: sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/utils@8.13.0':
+    resolution: {integrity: sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+
+  '@typescript-eslint/visitor-keys@8.13.0':
+    resolution: {integrity: sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@vitejs/plugin-vue@5.1.4':
+    resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    peerDependencies:
+      vite: ^5.0.0
+      vue: ^3.2.25
+
+  '@volar/language-core@2.4.8':
+    resolution: {integrity: sha512-K/GxMOXGq997bO00cdFhTNuR85xPxj0BEEAy+BaqqayTmy9Tmhfgmq2wpJcVspRhcwfgPoE2/mEJa26emUhG/g==}
+
+  '@volar/source-map@2.4.8':
+    resolution: {integrity: sha512-jeWJBkC/WivdelMwxKkpFL811uH/jJ1kVxa+c7OvG48DXc3VrP7pplSWPP2W1dLMqBxD+awRlg55FQQfiup4cA==}
+
+  '@volar/typescript@2.4.8':
+    resolution: {integrity: sha512-6xkIYJ5xxghVBhVywMoPMidDDAFT1OoQeXwa27HSgJ6AiIKRe61RXLoik+14Z7r0JvnblXVsjsRLmCr42SGzqg==}
+
+  '@vue/compiler-core@3.5.12':
+    resolution: {integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==}
+
+  '@vue/compiler-dom@3.5.12':
+    resolution: {integrity: sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==}
+
+  '@vue/compiler-sfc@3.5.12':
+    resolution: {integrity: sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==}
+
+  '@vue/compiler-ssr@3.5.12':
+    resolution: {integrity: sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==}
+
+  '@vue/compiler-vue2@2.7.16':
+    resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
+
+  '@vue/devtools-api@6.6.4':
+    resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
+
+  '@vue/eslint-config-prettier@10.1.0':
+    resolution: {integrity: sha512-J6wV91y2pXc0Phha01k0WOHBTPsoSTf4xlmMjoKaeSxBpAdsgTppGF5RZRdOHM7OA74zAXD+VLANrtYXpiPKkQ==}
+    peerDependencies:
+      eslint: '>= 8.21.0'
+      prettier: '>= 3.0.0'
+
+  '@vue/eslint-config-typescript@14.1.3':
+    resolution: {integrity: sha512-L4NUJQz/0We2QYtrNwRAGRy4KfpOagl5V3MpZZ+rQ51a+bKjlKYYrugi7lp7PIX8LolRgu06ZwDoswnSGWnAmA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^9.10.0
+      eslint-plugin-vue: ^9.28.0
+      typescript: '>=4.8.4'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@vue/language-core@2.1.10':
+    resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@vue/reactivity@3.5.12':
+    resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==}
+
+  '@vue/runtime-core@3.5.12':
+    resolution: {integrity: sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==}
+
+  '@vue/runtime-dom@3.5.12':
+    resolution: {integrity: sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==}
+
+  '@vue/server-renderer@3.5.12':
+    resolution: {integrity: sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==}
+    peerDependencies:
+      vue: 3.5.12
+
+  '@vue/shared@3.5.12':
+    resolution: {integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==}
+
+  '@vue/tsconfig@0.5.1':
+    resolution: {integrity: sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==}
+
+  '@vueuse/core@9.13.0':
+    resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
+
+  '@vueuse/metadata@9.13.0':
+    resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
+
+  '@vueuse/shared@9.13.0':
+    resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
+
+  acorn-jsx@5.3.2:
+    resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+    peerDependencies:
+      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+  acorn@8.14.0:
+    resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+
+  ajv@6.12.6:
+    resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+  alien-signals@0.2.0:
+    resolution: {integrity: sha512-StlonZhBBrsPPwrDjiPAiVTf/rolxffLxVPT60Qv/t88BZ81BvUVzHgGqEFvJ1ii8HXtm1+zU2Icr59tfWEcag==}
+
+  ansi-styles@4.3.0:
+    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+    engines: {node: '>=8'}
+
+  ansi-styles@6.2.1:
+    resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+    engines: {node: '>=12'}
+
+  anymatch@3.1.3:
+    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+    engines: {node: '>= 8'}
+
+  argparse@2.0.1:
+    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+  async-validator@4.2.5:
+    resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
+
+  asynckit@0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+  axios@1.7.7:
+    resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
+
+  balanced-match@1.0.2:
+    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+  binary-extensions@2.3.0:
+    resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+    engines: {node: '>=8'}
+
+  boolbase@1.0.0:
+    resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
+  brace-expansion@1.1.11:
+    resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+
+  brace-expansion@2.0.1:
+    resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+
+  braces@3.0.3:
+    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+    engines: {node: '>=8'}
+
+  callsites@3.1.0:
+    resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+    engines: {node: '>=6'}
+
+  chalk@4.1.2:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+    engines: {node: '>=10'}
+
+  chokidar@3.6.0:
+    resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+    engines: {node: '>= 8.10.0'}
+
+  color-convert@2.0.1:
+    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+    engines: {node: '>=7.0.0'}
+
+  color-name@1.1.4:
+    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+  combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+
+  concat-map@0.0.1:
+    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+  confbox@0.1.8:
+    resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
+
+  cross-spawn@7.0.3:
+    resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
+    engines: {node: '>= 8'}
+
+  cssesc@3.0.0:
+    resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+    engines: {node: '>=4'}
+    hasBin: true
+
+  csstype@3.1.3:
+    resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+  dayjs@1.11.13:
+    resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
+
+  de-indent@1.0.2:
+    resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
+
+  debug@4.3.7:
+    resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  deep-is@0.1.4:
+    resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+  delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+
+  element-plus@2.8.7:
+    resolution: {integrity: sha512-oGQyFRufFOgjd872tZc+T4xQAYLlX4hj6d3ixeY13L4fFNUuc1N49JHAqJGPda0tdx3qCnjceZoh1kqqj2+tXQ==}
+    peerDependencies:
+      vue: ^3.2.0
+
+  entities@4.5.0:
+    resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+    engines: {node: '>=0.12'}
+
+  esbuild@0.21.5:
+    resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
+    engines: {node: '>=12'}
+    hasBin: true
+
+  escape-html@1.0.3:
+    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
+  escape-string-regexp@4.0.0:
+    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+    engines: {node: '>=10'}
+
+  escape-string-regexp@5.0.0:
+    resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
+    engines: {node: '>=12'}
+
+  eslint-config-prettier@9.1.0:
+    resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
+    hasBin: true
+    peerDependencies:
+      eslint: '>=7.0.0'
+
+  eslint-plugin-prettier@5.2.1:
+    resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==}
+    engines: {node: ^14.18.0 || >=16.0.0}
+    peerDependencies:
+      '@types/eslint': '>=8.0.0'
+      eslint: '>=8.0.0'
+      eslint-config-prettier: '*'
+      prettier: '>=3.0.0'
+    peerDependenciesMeta:
+      '@types/eslint':
+        optional: true
+      eslint-config-prettier:
+        optional: true
+
+  eslint-plugin-vue@9.30.0:
+    resolution: {integrity: sha512-CyqlRgShvljFkOeYK8wN5frh/OGTvkj1S7wlr2Q2pUvwq+X5VYiLd6ZjujpgSgLnys2W8qrBLkXQ41SUYaoPIQ==}
+    engines: {node: ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
+
+  eslint-scope@7.2.2:
+    resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  eslint-scope@8.2.0:
+    resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  eslint-visitor-keys@3.4.3:
+    resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  eslint-visitor-keys@4.2.0:
+    resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  eslint@9.14.0:
+    resolution: {integrity: sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    hasBin: true
+    peerDependencies:
+      jiti: '*'
+    peerDependenciesMeta:
+      jiti:
+        optional: true
+
+  espree@10.3.0:
+    resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  espree@9.6.1:
+    resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  esquery@1.6.0:
+    resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+    engines: {node: '>=0.10'}
+
+  esrecurse@4.3.0:
+    resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+    engines: {node: '>=4.0'}
+
+  estraverse@5.3.0:
+    resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+    engines: {node: '>=4.0'}
+
+  estree-walker@2.0.2:
+    resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+  estree-walker@3.0.3:
+    resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+  esutils@2.0.3:
+    resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+    engines: {node: '>=0.10.0'}
+
+  fast-deep-equal@3.1.3:
+    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+  fast-diff@1.3.0:
+    resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
+
+  fast-glob@3.3.2:
+    resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
+    engines: {node: '>=8.6.0'}
+
+  fast-json-stable-stringify@2.1.0:
+    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+  fast-levenshtein@2.0.6:
+    resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+  fastq@1.17.1:
+    resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
+
+  file-entry-cache@8.0.0:
+    resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+    engines: {node: '>=16.0.0'}
+
+  fill-range@7.1.1:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+    engines: {node: '>=8'}
+
+  find-up@5.0.0:
+    resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+    engines: {node: '>=10'}
+
+  flat-cache@4.0.1:
+    resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+    engines: {node: '>=16'}
+
+  flatted@3.3.1:
+    resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
+
+  follow-redirects@1.15.9:
+    resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
+  form-data@4.0.1:
+    resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
+    engines: {node: '>= 6'}
+
+  fsevents@2.3.3:
+    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+
+  glob-parent@5.1.2:
+    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+    engines: {node: '>= 6'}
+
+  glob-parent@6.0.2:
+    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+    engines: {node: '>=10.13.0'}
+
+  globals@13.24.0:
+    resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
+    engines: {node: '>=8'}
+
+  globals@14.0.0:
+    resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+    engines: {node: '>=18'}
+
+  graphemer@1.4.0:
+    resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+
+  has-flag@4.0.0:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+    engines: {node: '>=8'}
+
+  he@1.2.0:
+    resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
+    hasBin: true
+
+  ignore@5.3.2:
+    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+    engines: {node: '>= 4'}
+
+  import-fresh@3.3.0:
+    resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
+    engines: {node: '>=6'}
+
+  imurmurhash@0.1.4:
+    resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+    engines: {node: '>=0.8.19'}
+
+  is-binary-path@2.1.0:
+    resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+    engines: {node: '>=8'}
+
+  is-extglob@2.1.1:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+    engines: {node: '>=0.10.0'}
+
+  is-glob@4.0.3:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+    engines: {node: '>=0.10.0'}
+
+  is-number@7.0.0:
+    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+    engines: {node: '>=0.12.0'}
+
+  isexe@2.0.0:
+    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+  isexe@3.1.1:
+    resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
+    engines: {node: '>=16'}
+
+  js-tokens@9.0.0:
+    resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==}
+
+  js-yaml@4.1.0:
+    resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+    hasBin: true
+
+  json-buffer@3.0.1:
+    resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+  json-parse-even-better-errors@4.0.0:
+    resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==}
+    engines: {node: ^18.17.0 || >=20.5.0}
+
+  json-schema-traverse@0.4.1:
+    resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+  json-stable-stringify-without-jsonify@1.0.1:
+    resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+  keyv@4.5.4:
+    resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+  levn@0.4.1:
+    resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+    engines: {node: '>= 0.8.0'}
+
+  local-pkg@0.5.0:
+    resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
+    engines: {node: '>=14'}
+
+  locate-path@6.0.0:
+    resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+    engines: {node: '>=10'}
+
+  lodash-es@4.17.21:
+    resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
+
+  lodash-unified@1.0.3:
+    resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
+    peerDependencies:
+      '@types/lodash-es': '*'
+      lodash: '*'
+      lodash-es: '*'
+
+  lodash.merge@4.6.2:
+    resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+  lodash@4.17.21:
+    resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+
+  magic-string@0.30.12:
+    resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}
+
+  memoize-one@6.0.0:
+    resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+
+  memorystream@0.3.1:
+    resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
+    engines: {node: '>= 0.10.0'}
+
+  merge2@1.4.1:
+    resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+    engines: {node: '>= 8'}
+
+  micromatch@4.0.8:
+    resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+    engines: {node: '>=8.6'}
+
+  mime-db@1.52.0:
+    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+    engines: {node: '>= 0.6'}
+
+  mime-types@2.1.35:
+    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+    engines: {node: '>= 0.6'}
+
+  minimatch@3.1.2:
+    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+  minimatch@9.0.5:
+    resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+    engines: {node: '>=16 || 14 >=14.17'}
+
+  mlly@1.7.3:
+    resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==}
+
+  ms@2.1.3:
+    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+  muggle-string@0.4.1:
+    resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
+
+  nanoid@3.3.7:
+    resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
+  natural-compare@1.4.0:
+    resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+  normalize-path@3.0.0:
+    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+    engines: {node: '>=0.10.0'}
+
+  normalize-wheel-es@1.2.0:
+    resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
+
+  npm-normalize-package-bin@4.0.0:
+    resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==}
+    engines: {node: ^18.17.0 || >=20.5.0}
+
+  npm-run-all2@7.0.1:
+    resolution: {integrity: sha512-Adbv+bJQ8UTAM03rRODqrO5cx0YU5KCG2CvHtSURiadvdTjjgGJXdbc1oQ9CXBh9dnGfHSoSB1Web/0Dzp6kOQ==}
+    engines: {node: ^18.17.0 || >=20.5.0, npm: '>= 9'}
+    hasBin: true
+
+  nth-check@2.1.1:
+    resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+  optionator@0.9.4:
+    resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+    engines: {node: '>= 0.8.0'}
+
+  p-limit@3.1.0:
+    resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+    engines: {node: '>=10'}
+
+  p-locate@5.0.0:
+    resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+    engines: {node: '>=10'}
+
+  parent-module@1.0.1:
+    resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+    engines: {node: '>=6'}
+
+  path-browserify@1.0.1:
+    resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
+  path-exists@4.0.0:
+    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+    engines: {node: '>=8'}
+
+  path-key@3.1.1:
+    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+    engines: {node: '>=8'}
+
+  pathe@1.1.2:
+    resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+
+  picocolors@1.1.1:
+    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+  picomatch@2.3.1:
+    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+    engines: {node: '>=8.6'}
+
+  picomatch@4.0.2:
+    resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
+    engines: {node: '>=12'}
+
+  pidtree@0.6.0:
+    resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
+    engines: {node: '>=0.10'}
+    hasBin: true
+
+  pinia@2.2.6:
+    resolution: {integrity: sha512-vIsR8JkDN5Ga2vAxqOE2cJj4VtsHnzpR1Fz30kClxlh0yCHfec6uoMeM3e/ddqmwFUejK3NlrcQa/shnpyT4hA==}
+    peerDependencies:
+      '@vue/composition-api': ^1.4.0
+      typescript: '>=4.4.4'
+      vue: ^2.6.14 || ^3.5.11
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+      typescript:
+        optional: true
+
+  pkg-types@1.2.1:
+    resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==}
+
+  postcss-selector-parser@6.1.2:
+    resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
+    engines: {node: '>=4'}
+
+  postcss@8.4.47:
+    resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
+    engines: {node: ^10 || ^12 || >=14}
+
+  prelude-ls@1.2.1:
+    resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+    engines: {node: '>= 0.8.0'}
+
+  prettier-linter-helpers@1.0.0:
+    resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
+    engines: {node: '>=6.0.0'}
+
+  prettier@3.3.3:
+    resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==}
+    engines: {node: '>=14'}
+    hasBin: true
+
+  proxy-from-env@1.1.0:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+  punycode@2.3.1:
+    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+    engines: {node: '>=6'}
+
+  queue-microtask@1.2.3:
+    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+  read-package-json-fast@4.0.0:
+    resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==}
+    engines: {node: ^18.17.0 || >=20.5.0}
+
+  readdirp@3.6.0:
+    resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+    engines: {node: '>=8.10.0'}
+
+  resolve-from@4.0.0:
+    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+    engines: {node: '>=4'}
+
+  reusify@1.0.4:
+    resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+    engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+  rollup@4.24.4:
+    resolution: {integrity: sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==}
+    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+    hasBin: true
+
+  run-parallel@1.2.0:
+    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+  scule@1.3.0:
+    resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
+
+  semver@7.6.3:
+    resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
+    engines: {node: '>=10'}
+    hasBin: true
+
+  shebang-command@2.0.0:
+    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+    engines: {node: '>=8'}
+
+  shebang-regex@3.0.0:
+    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+    engines: {node: '>=8'}
+
+  shell-quote@1.8.1:
+    resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==}
+
+  source-map-js@1.2.1:
+    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+    engines: {node: '>=0.10.0'}
+
+  strip-json-comments@3.1.1:
+    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+    engines: {node: '>=8'}
+
+  strip-literal@2.1.0:
+    resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==}
+
+  supports-color@7.2.0:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+
+  synckit@0.9.2:
+    resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
+    engines: {node: ^14.18.0 || >=16.0.0}
+
+  text-table@0.2.0:
+    resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
+
+  to-regex-range@5.0.1:
+    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+    engines: {node: '>=8.0'}
+
+  ts-api-utils@1.4.0:
+    resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      typescript: '>=4.2.0'
+
+  tslib@2.8.1:
+    resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+  type-check@0.4.0:
+    resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+    engines: {node: '>= 0.8.0'}
+
+  type-fest@0.20.2:
+    resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
+    engines: {node: '>=10'}
+
+  typescript-eslint@8.13.0:
+    resolution: {integrity: sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  typescript@5.6.3:
+    resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  ufo@1.5.4:
+    resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
+
+  undici-types@6.19.8:
+    resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
+
+  unimport@3.13.1:
+    resolution: {integrity: sha512-nNrVzcs93yrZQOW77qnyOVHtb68LegvhYFwxFMfuuWScmwQmyVCG/NBuN8tYsaGzgQUVYv34E/af+Cc9u4og4A==}
+
+  unplugin-auto-import@0.18.3:
+    resolution: {integrity: sha512-q3FUtGQjYA2e+kb1WumyiQMjHM27MrTQ05QfVwtLRVhyYe+KF6TblBYaEX9L6Z0EibsqaXAiW+RFfkcQpfaXzg==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@nuxt/kit': ^3.2.2
+      '@vueuse/core': '*'
+    peerDependenciesMeta:
+      '@nuxt/kit':
+        optional: true
+      '@vueuse/core':
+        optional: true
+
+  unplugin-vue-components@0.27.4:
+    resolution: {integrity: sha512-1XVl5iXG7P1UrOMnaj2ogYa5YTq8aoh5jwDPQhemwO/OrXW+lPQKDXd1hMz15qxQPxgb/XXlbgo3HQ2rLEbmXQ==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@babel/parser': ^7.15.8
+      '@nuxt/kit': ^3.2.2
+      vue: 2 || 3
+    peerDependenciesMeta:
+      '@babel/parser':
+        optional: true
+      '@nuxt/kit':
+        optional: true
+
+  unplugin@1.15.0:
+    resolution: {integrity: sha512-jTPIs63W+DUEDW207ztbaoO7cQ4p5aVaB823LSlxpsFEU3Mykwxf3ZGC/wzxFJeZlASZYgVrWeo7LgOrqJZ8RA==}
+    engines: {node: '>=14.0.0'}
+    peerDependencies:
+      webpack-sources: ^3
+    peerDependenciesMeta:
+      webpack-sources:
+        optional: true
+
+  uri-js@4.4.1:
+    resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+  util-deprecate@1.0.2:
+    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+  vite@5.4.10:
+    resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^18.0.0 || >=20.0.0
+      less: '*'
+      lightningcss: ^1.21.0
+      sass: '*'
+      sass-embedded: '*'
+      stylus: '*'
+      sugarss: '*'
+      terser: ^5.4.0
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      sass-embedded:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+
+  vscode-uri@3.0.8:
+    resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
+
+  vue-demi@0.14.10:
+    resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
+    engines: {node: '>=12'}
+    hasBin: true
+    peerDependencies:
+      '@vue/composition-api': ^1.0.0-rc.1
+      vue: ^3.0.0-0 || ^2.6.0
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+
+  vue-eslint-parser@9.4.3:
+    resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==}
+    engines: {node: ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: '>=6.0.0'
+
+  vue-router@4.4.5:
+    resolution: {integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==}
+    peerDependencies:
+      vue: ^3.2.0
+
+  vue-tsc@2.1.10:
+    resolution: {integrity: sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==}
+    hasBin: true
+    peerDependencies:
+      typescript: '>=5.0.0'
+
+  vue@3.5.12:
+    resolution: {integrity: sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  webpack-virtual-modules@0.6.2:
+    resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+
+  which@2.0.2:
+    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+    engines: {node: '>= 8'}
+    hasBin: true
+
+  which@5.0.0:
+    resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==}
+    engines: {node: ^18.17.0 || >=20.5.0}
+    hasBin: true
+
+  word-wrap@1.2.5:
+    resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+    engines: {node: '>=0.10.0'}
+
+  xml-name-validator@4.0.0:
+    resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
+    engines: {node: '>=12'}
+
+  yocto-queue@0.1.0:
+    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+    engines: {node: '>=10'}
+
+snapshots:
+
+  '@antfu/utils@0.7.10': {}
+
+  '@babel/helper-string-parser@7.25.9': {}
+
+  '@babel/helper-validator-identifier@7.25.9': {}
+
+  '@babel/parser@7.26.2':
+    dependencies:
+      '@babel/types': 7.26.0
+
+  '@babel/types@7.26.0':
+    dependencies:
+      '@babel/helper-string-parser': 7.25.9
+      '@babel/helper-validator-identifier': 7.25.9
+
+  '@ctrl/tinycolor@3.6.1': {}
+
+  '@element-plus/icons-vue@2.3.1(vue@3.5.12(typescript@5.6.3))':
+    dependencies:
+      vue: 3.5.12(typescript@5.6.3)
+
+  '@esbuild/aix-ppc64@0.21.5':
+    optional: true
+
+  '@esbuild/android-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/android-arm@0.21.5':
+    optional: true
+
+  '@esbuild/android-x64@0.21.5':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/darwin-x64@0.21.5':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-arm@0.21.5':
+    optional: true
+
+  '@esbuild/linux-ia32@0.21.5':
+    optional: true
+
+  '@esbuild/linux-loong64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.21.5':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-s390x@0.21.5':
+    optional: true
+
+  '@esbuild/linux-x64@0.21.5':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/sunos-x64@0.21.5':
+    optional: true
+
+  '@esbuild/win32-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/win32-ia32@0.21.5':
+    optional: true
+
+  '@esbuild/win32-x64@0.21.5':
+    optional: true
+
+  '@eslint-community/eslint-utils@4.4.1(eslint@9.14.0)':
+    dependencies:
+      eslint: 9.14.0
+      eslint-visitor-keys: 3.4.3
+
+  '@eslint-community/regexpp@4.12.1': {}
+
+  '@eslint/config-array@0.18.0':
+    dependencies:
+      '@eslint/object-schema': 2.1.4
+      debug: 4.3.7
+      minimatch: 3.1.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/core@0.7.0': {}
+
+  '@eslint/eslintrc@3.1.0':
+    dependencies:
+      ajv: 6.12.6
+      debug: 4.3.7
+      espree: 10.3.0
+      globals: 14.0.0
+      ignore: 5.3.2
+      import-fresh: 3.3.0
+      js-yaml: 4.1.0
+      minimatch: 3.1.2
+      strip-json-comments: 3.1.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/js@9.14.0': {}
+
+  '@eslint/object-schema@2.1.4': {}
+
+  '@eslint/plugin-kit@0.2.2':
+    dependencies:
+      levn: 0.4.1
+
+  '@floating-ui/core@1.6.8':
+    dependencies:
+      '@floating-ui/utils': 0.2.8
+
+  '@floating-ui/dom@1.6.12':
+    dependencies:
+      '@floating-ui/core': 1.6.8
+      '@floating-ui/utils': 0.2.8
+
+  '@floating-ui/utils@0.2.8': {}
+
+  '@humanfs/core@0.19.1': {}
+
+  '@humanfs/node@0.16.6':
+    dependencies:
+      '@humanfs/core': 0.19.1
+      '@humanwhocodes/retry': 0.3.1
+
+  '@humanwhocodes/module-importer@1.0.1': {}
+
+  '@humanwhocodes/retry@0.3.1': {}
+
+  '@humanwhocodes/retry@0.4.1': {}
+
+  '@jridgewell/sourcemap-codec@1.5.0': {}
+
+  '@nodelib/fs.scandir@2.1.5':
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      run-parallel: 1.2.0
+
+  '@nodelib/fs.stat@2.0.5': {}
+
+  '@nodelib/fs.walk@1.2.8':
+    dependencies:
+      '@nodelib/fs.scandir': 2.1.5
+      fastq: 1.17.1
+
+  '@pkgr/core@0.1.1': {}
+
+  '@rollup/pluginutils@5.1.3(rollup@4.24.4)':
+    dependencies:
+      '@types/estree': 1.0.6
+      estree-walker: 2.0.2
+      picomatch: 4.0.2
+    optionalDependencies:
+      rollup: 4.24.4
+
+  '@rollup/rollup-android-arm-eabi@4.24.4':
+    optional: true
+
+  '@rollup/rollup-android-arm64@4.24.4':
+    optional: true
+
+  '@rollup/rollup-darwin-arm64@4.24.4':
+    optional: true
+
+  '@rollup/rollup-darwin-x64@4.24.4':
+    optional: true
+
+  '@rollup/rollup-freebsd-arm64@4.24.4':
+    optional: true
+
+  '@rollup/rollup-freebsd-x64@4.24.4':
+    optional: true
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.24.4':
+    optional: true
+
+  '@rollup/rollup-linux-arm-musleabihf@4.24.4':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-gnu@4.24.4':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-musl@4.24.4':
+    optional: true
+
+  '@rollup/rollup-linux-powerpc64le-gnu@4.24.4':
+    optional: true
+
+  '@rollup/rollup-linux-riscv64-gnu@4.24.4':
+    optional: true
+
+  '@rollup/rollup-linux-s390x-gnu@4.24.4':
+    optional: true
+
+  '@rollup/rollup-linux-x64-gnu@4.24.4':
+    optional: true
+
+  '@rollup/rollup-linux-x64-musl@4.24.4':
+    optional: true
+
+  '@rollup/rollup-win32-arm64-msvc@4.24.4':
+    optional: true
+
+  '@rollup/rollup-win32-ia32-msvc@4.24.4':
+    optional: true
+
+  '@rollup/rollup-win32-x64-msvc@4.24.4':
+    optional: true
+
+  '@sxzz/popperjs-es@2.11.7': {}
+
+  '@tsconfig/node20@20.1.4': {}
+
+  '@types/estree@1.0.6': {}
+
+  '@types/json-schema@7.0.15': {}
+
+  '@types/lodash-es@4.17.12':
+    dependencies:
+      '@types/lodash': 4.17.13
+
+  '@types/lodash@4.17.13': {}
+
+  '@types/node@20.17.6':
+    dependencies:
+      undici-types: 6.19.8
+
+  '@types/web-bluetooth@0.0.16': {}
+
+  '@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)':
+    dependencies:
+      '@eslint-community/regexpp': 4.12.1
+      '@typescript-eslint/parser': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
+      '@typescript-eslint/scope-manager': 8.13.0
+      '@typescript-eslint/type-utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
+      '@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
+      '@typescript-eslint/visitor-keys': 8.13.0
+      eslint: 9.14.0
+      graphemer: 1.4.0
+      ignore: 5.3.2
+      natural-compare: 1.4.0
+      ts-api-utils: 1.4.0(typescript@5.6.3)
+    optionalDependencies:
+      typescript: 5.6.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3)':
+    dependencies:
+      '@typescript-eslint/scope-manager': 8.13.0
+      '@typescript-eslint/types': 8.13.0
+      '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
+      '@typescript-eslint/visitor-keys': 8.13.0
+      debug: 4.3.7
+      eslint: 9.14.0
+    optionalDependencies:
+      typescript: 5.6.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/scope-manager@8.13.0':
+    dependencies:
+      '@typescript-eslint/types': 8.13.0
+      '@typescript-eslint/visitor-keys': 8.13.0
+
+  '@typescript-eslint/type-utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)':
+    dependencies:
+      '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
+      '@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
+      debug: 4.3.7
+      ts-api-utils: 1.4.0(typescript@5.6.3)
+    optionalDependencies:
+      typescript: 5.6.3
+    transitivePeerDependencies:
+      - eslint
+      - supports-color
+
+  '@typescript-eslint/types@8.13.0': {}
+
+  '@typescript-eslint/typescript-estree@8.13.0(typescript@5.6.3)':
+    dependencies:
+      '@typescript-eslint/types': 8.13.0
+      '@typescript-eslint/visitor-keys': 8.13.0
+      debug: 4.3.7
+      fast-glob: 3.3.2
+      is-glob: 4.0.3
+      minimatch: 9.0.5
+      semver: 7.6.3
+      ts-api-utils: 1.4.0(typescript@5.6.3)
+    optionalDependencies:
+      typescript: 5.6.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)':
+    dependencies:
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0)
+      '@typescript-eslint/scope-manager': 8.13.0
+      '@typescript-eslint/types': 8.13.0
+      '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
+      eslint: 9.14.0
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+
+  '@typescript-eslint/visitor-keys@8.13.0':
+    dependencies:
+      '@typescript-eslint/types': 8.13.0
+      eslint-visitor-keys: 3.4.3
+
+  '@vitejs/plugin-vue@5.1.4(vite@5.4.10(@types/node@20.17.6))(vue@3.5.12(typescript@5.6.3))':
+    dependencies:
+      vite: 5.4.10(@types/node@20.17.6)
+      vue: 3.5.12(typescript@5.6.3)
+
+  '@volar/language-core@2.4.8':
+    dependencies:
+      '@volar/source-map': 2.4.8
+
+  '@volar/source-map@2.4.8': {}
+
+  '@volar/typescript@2.4.8':
+    dependencies:
+      '@volar/language-core': 2.4.8
+      path-browserify: 1.0.1
+      vscode-uri: 3.0.8
+
+  '@vue/compiler-core@3.5.12':
+    dependencies:
+      '@babel/parser': 7.26.2
+      '@vue/shared': 3.5.12
+      entities: 4.5.0
+      estree-walker: 2.0.2
+      source-map-js: 1.2.1
+
+  '@vue/compiler-dom@3.5.12':
+    dependencies:
+      '@vue/compiler-core': 3.5.12
+      '@vue/shared': 3.5.12
+
+  '@vue/compiler-sfc@3.5.12':
+    dependencies:
+      '@babel/parser': 7.26.2
+      '@vue/compiler-core': 3.5.12
+      '@vue/compiler-dom': 3.5.12
+      '@vue/compiler-ssr': 3.5.12
+      '@vue/shared': 3.5.12
+      estree-walker: 2.0.2
+      magic-string: 0.30.12
+      postcss: 8.4.47
+      source-map-js: 1.2.1
+
+  '@vue/compiler-ssr@3.5.12':
+    dependencies:
+      '@vue/compiler-dom': 3.5.12
+      '@vue/shared': 3.5.12
+
+  '@vue/compiler-vue2@2.7.16':
+    dependencies:
+      de-indent: 1.0.2
+      he: 1.2.0
+
+  '@vue/devtools-api@6.6.4': {}
+
+  '@vue/eslint-config-prettier@10.1.0(eslint@9.14.0)(prettier@3.3.3)':
+    dependencies:
+      eslint: 9.14.0
+      eslint-config-prettier: 9.1.0(eslint@9.14.0)
+      eslint-plugin-prettier: 5.2.1(eslint-config-prettier@9.1.0(eslint@9.14.0))(eslint@9.14.0)(prettier@3.3.3)
+      prettier: 3.3.3
+    transitivePeerDependencies:
+      - '@types/eslint'
+
+  '@vue/eslint-config-typescript@14.1.3(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint-plugin-vue@9.30.0(eslint@9.14.0))(eslint@9.14.0)(typescript@5.6.3)':
+    dependencies:
+      '@typescript-eslint/eslint-plugin': 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
+      eslint: 9.14.0
+      eslint-plugin-vue: 9.30.0(eslint@9.14.0)
+      fast-glob: 3.3.2
+      typescript-eslint: 8.13.0(eslint@9.14.0)(typescript@5.6.3)
+      vue-eslint-parser: 9.4.3(eslint@9.14.0)
+    optionalDependencies:
+      typescript: 5.6.3
+    transitivePeerDependencies:
+      - '@typescript-eslint/parser'
+      - supports-color
+
+  '@vue/language-core@2.1.10(typescript@5.6.3)':
+    dependencies:
+      '@volar/language-core': 2.4.8
+      '@vue/compiler-dom': 3.5.12
+      '@vue/compiler-vue2': 2.7.16
+      '@vue/shared': 3.5.12
+      alien-signals: 0.2.0
+      minimatch: 9.0.5
+      muggle-string: 0.4.1
+      path-browserify: 1.0.1
+    optionalDependencies:
+      typescript: 5.6.3
+
+  '@vue/reactivity@3.5.12':
+    dependencies:
+      '@vue/shared': 3.5.12
+
+  '@vue/runtime-core@3.5.12':
+    dependencies:
+      '@vue/reactivity': 3.5.12
+      '@vue/shared': 3.5.12
+
+  '@vue/runtime-dom@3.5.12':
+    dependencies:
+      '@vue/reactivity': 3.5.12
+      '@vue/runtime-core': 3.5.12
+      '@vue/shared': 3.5.12
+      csstype: 3.1.3
+
+  '@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3))':
+    dependencies:
+      '@vue/compiler-ssr': 3.5.12
+      '@vue/shared': 3.5.12
+      vue: 3.5.12(typescript@5.6.3)
+
+  '@vue/shared@3.5.12': {}
+
+  '@vue/tsconfig@0.5.1': {}
+
+  '@vueuse/core@9.13.0(vue@3.5.12(typescript@5.6.3))':
+    dependencies:
+      '@types/web-bluetooth': 0.0.16
+      '@vueuse/metadata': 9.13.0
+      '@vueuse/shared': 9.13.0(vue@3.5.12(typescript@5.6.3))
+      vue-demi: 0.14.10(vue@3.5.12(typescript@5.6.3))
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+
+  '@vueuse/metadata@9.13.0': {}
+
+  '@vueuse/shared@9.13.0(vue@3.5.12(typescript@5.6.3))':
+    dependencies:
+      vue-demi: 0.14.10(vue@3.5.12(typescript@5.6.3))
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+
+  acorn-jsx@5.3.2(acorn@8.14.0):
+    dependencies:
+      acorn: 8.14.0
+
+  acorn@8.14.0: {}
+
+  ajv@6.12.6:
+    dependencies:
+      fast-deep-equal: 3.1.3
+      fast-json-stable-stringify: 2.1.0
+      json-schema-traverse: 0.4.1
+      uri-js: 4.4.1
+
+  alien-signals@0.2.0: {}
+
+  ansi-styles@4.3.0:
+    dependencies:
+      color-convert: 2.0.1
+
+  ansi-styles@6.2.1: {}
+
+  anymatch@3.1.3:
+    dependencies:
+      normalize-path: 3.0.0
+      picomatch: 2.3.1
+
+  argparse@2.0.1: {}
+
+  async-validator@4.2.5: {}
+
+  asynckit@0.4.0: {}
+
+  axios@1.7.7:
+    dependencies:
+      follow-redirects: 1.15.9
+      form-data: 4.0.1
+      proxy-from-env: 1.1.0
+    transitivePeerDependencies:
+      - debug
+
+  balanced-match@1.0.2: {}
+
+  binary-extensions@2.3.0: {}
+
+  boolbase@1.0.0: {}
+
+  brace-expansion@1.1.11:
+    dependencies:
+      balanced-match: 1.0.2
+      concat-map: 0.0.1
+
+  brace-expansion@2.0.1:
+    dependencies:
+      balanced-match: 1.0.2
+
+  braces@3.0.3:
+    dependencies:
+      fill-range: 7.1.1
+
+  callsites@3.1.0: {}
+
+  chalk@4.1.2:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+
+  chokidar@3.6.0:
+    dependencies:
+      anymatch: 3.1.3
+      braces: 3.0.3
+      glob-parent: 5.1.2
+      is-binary-path: 2.1.0
+      is-glob: 4.0.3
+      normalize-path: 3.0.0
+      readdirp: 3.6.0
+    optionalDependencies:
+      fsevents: 2.3.3
+
+  color-convert@2.0.1:
+    dependencies:
+      color-name: 1.1.4
+
+  color-name@1.1.4: {}
+
+  combined-stream@1.0.8:
+    dependencies:
+      delayed-stream: 1.0.0
+
+  concat-map@0.0.1: {}
+
+  confbox@0.1.8: {}
+
+  cross-spawn@7.0.3:
+    dependencies:
+      path-key: 3.1.1
+      shebang-command: 2.0.0
+      which: 2.0.2
+
+  cssesc@3.0.0: {}
+
+  csstype@3.1.3: {}
+
+  dayjs@1.11.13: {}
+
+  de-indent@1.0.2: {}
+
+  debug@4.3.7:
+    dependencies:
+      ms: 2.1.3
+
+  deep-is@0.1.4: {}
+
+  delayed-stream@1.0.0: {}
+
+  element-plus@2.8.7(vue@3.5.12(typescript@5.6.3)):
+    dependencies:
+      '@ctrl/tinycolor': 3.6.1
+      '@element-plus/icons-vue': 2.3.1(vue@3.5.12(typescript@5.6.3))
+      '@floating-ui/dom': 1.6.12
+      '@popperjs/core': '@sxzz/popperjs-es@2.11.7'
+      '@types/lodash': 4.17.13
+      '@types/lodash-es': 4.17.12
+      '@vueuse/core': 9.13.0(vue@3.5.12(typescript@5.6.3))
+      async-validator: 4.2.5
+      dayjs: 1.11.13
+      escape-html: 1.0.3
+      lodash: 4.17.21
+      lodash-es: 4.17.21
+      lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21)
+      memoize-one: 6.0.0
+      normalize-wheel-es: 1.2.0
+      vue: 3.5.12(typescript@5.6.3)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+
+  entities@4.5.0: {}
+
+  esbuild@0.21.5:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.21.5
+      '@esbuild/android-arm': 0.21.5
+      '@esbuild/android-arm64': 0.21.5
+      '@esbuild/android-x64': 0.21.5
+      '@esbuild/darwin-arm64': 0.21.5
+      '@esbuild/darwin-x64': 0.21.5
+      '@esbuild/freebsd-arm64': 0.21.5
+      '@esbuild/freebsd-x64': 0.21.5
+      '@esbuild/linux-arm': 0.21.5
+      '@esbuild/linux-arm64': 0.21.5
+      '@esbuild/linux-ia32': 0.21.5
+      '@esbuild/linux-loong64': 0.21.5
+      '@esbuild/linux-mips64el': 0.21.5
+      '@esbuild/linux-ppc64': 0.21.5
+      '@esbuild/linux-riscv64': 0.21.5
+      '@esbuild/linux-s390x': 0.21.5
+      '@esbuild/linux-x64': 0.21.5
+      '@esbuild/netbsd-x64': 0.21.5
+      '@esbuild/openbsd-x64': 0.21.5
+      '@esbuild/sunos-x64': 0.21.5
+      '@esbuild/win32-arm64': 0.21.5
+      '@esbuild/win32-ia32': 0.21.5
+      '@esbuild/win32-x64': 0.21.5
+
+  escape-html@1.0.3: {}
+
+  escape-string-regexp@4.0.0: {}
+
+  escape-string-regexp@5.0.0: {}
+
+  eslint-config-prettier@9.1.0(eslint@9.14.0):
+    dependencies:
+      eslint: 9.14.0
+
+  eslint-plugin-prettier@5.2.1(eslint-config-prettier@9.1.0(eslint@9.14.0))(eslint@9.14.0)(prettier@3.3.3):
+    dependencies:
+      eslint: 9.14.0
+      prettier: 3.3.3
+      prettier-linter-helpers: 1.0.0
+      synckit: 0.9.2
+    optionalDependencies:
+      eslint-config-prettier: 9.1.0(eslint@9.14.0)
+
+  eslint-plugin-vue@9.30.0(eslint@9.14.0):
+    dependencies:
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0)
+      eslint: 9.14.0
+      globals: 13.24.0
+      natural-compare: 1.4.0
+      nth-check: 2.1.1
+      postcss-selector-parser: 6.1.2
+      semver: 7.6.3
+      vue-eslint-parser: 9.4.3(eslint@9.14.0)
+      xml-name-validator: 4.0.0
+    transitivePeerDependencies:
+      - supports-color
+
+  eslint-scope@7.2.2:
+    dependencies:
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
+  eslint-scope@8.2.0:
+    dependencies:
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
+  eslint-visitor-keys@3.4.3: {}
+
+  eslint-visitor-keys@4.2.0: {}
+
+  eslint@9.14.0:
+    dependencies:
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0)
+      '@eslint-community/regexpp': 4.12.1
+      '@eslint/config-array': 0.18.0
+      '@eslint/core': 0.7.0
+      '@eslint/eslintrc': 3.1.0
+      '@eslint/js': 9.14.0
+      '@eslint/plugin-kit': 0.2.2
+      '@humanfs/node': 0.16.6
+      '@humanwhocodes/module-importer': 1.0.1
+      '@humanwhocodes/retry': 0.4.1
+      '@types/estree': 1.0.6
+      '@types/json-schema': 7.0.15
+      ajv: 6.12.6
+      chalk: 4.1.2
+      cross-spawn: 7.0.3
+      debug: 4.3.7
+      escape-string-regexp: 4.0.0
+      eslint-scope: 8.2.0
+      eslint-visitor-keys: 4.2.0
+      espree: 10.3.0
+      esquery: 1.6.0
+      esutils: 2.0.3
+      fast-deep-equal: 3.1.3
+      file-entry-cache: 8.0.0
+      find-up: 5.0.0
+      glob-parent: 6.0.2
+      ignore: 5.3.2
+      imurmurhash: 0.1.4
+      is-glob: 4.0.3
+      json-stable-stringify-without-jsonify: 1.0.1
+      lodash.merge: 4.6.2
+      minimatch: 3.1.2
+      natural-compare: 1.4.0
+      optionator: 0.9.4
+      text-table: 0.2.0
+    transitivePeerDependencies:
+      - supports-color
+
+  espree@10.3.0:
+    dependencies:
+      acorn: 8.14.0
+      acorn-jsx: 5.3.2(acorn@8.14.0)
+      eslint-visitor-keys: 4.2.0
+
+  espree@9.6.1:
+    dependencies:
+      acorn: 8.14.0
+      acorn-jsx: 5.3.2(acorn@8.14.0)
+      eslint-visitor-keys: 3.4.3
+
+  esquery@1.6.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  esrecurse@4.3.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  estraverse@5.3.0: {}
+
+  estree-walker@2.0.2: {}
+
+  estree-walker@3.0.3:
+    dependencies:
+      '@types/estree': 1.0.6
+
+  esutils@2.0.3: {}
+
+  fast-deep-equal@3.1.3: {}
+
+  fast-diff@1.3.0: {}
+
+  fast-glob@3.3.2:
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      '@nodelib/fs.walk': 1.2.8
+      glob-parent: 5.1.2
+      merge2: 1.4.1
+      micromatch: 4.0.8
+
+  fast-json-stable-stringify@2.1.0: {}
+
+  fast-levenshtein@2.0.6: {}
+
+  fastq@1.17.1:
+    dependencies:
+      reusify: 1.0.4
+
+  file-entry-cache@8.0.0:
+    dependencies:
+      flat-cache: 4.0.1
+
+  fill-range@7.1.1:
+    dependencies:
+      to-regex-range: 5.0.1
+
+  find-up@5.0.0:
+    dependencies:
+      locate-path: 6.0.0
+      path-exists: 4.0.0
+
+  flat-cache@4.0.1:
+    dependencies:
+      flatted: 3.3.1
+      keyv: 4.5.4
+
+  flatted@3.3.1: {}
+
+  follow-redirects@1.15.9: {}
+
+  form-data@4.0.1:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      mime-types: 2.1.35
+
+  fsevents@2.3.3:
+    optional: true
+
+  glob-parent@5.1.2:
+    dependencies:
+      is-glob: 4.0.3
+
+  glob-parent@6.0.2:
+    dependencies:
+      is-glob: 4.0.3
+
+  globals@13.24.0:
+    dependencies:
+      type-fest: 0.20.2
+
+  globals@14.0.0: {}
+
+  graphemer@1.4.0: {}
+
+  has-flag@4.0.0: {}
+
+  he@1.2.0: {}
+
+  ignore@5.3.2: {}
+
+  import-fresh@3.3.0:
+    dependencies:
+      parent-module: 1.0.1
+      resolve-from: 4.0.0
+
+  imurmurhash@0.1.4: {}
+
+  is-binary-path@2.1.0:
+    dependencies:
+      binary-extensions: 2.3.0
+
+  is-extglob@2.1.1: {}
+
+  is-glob@4.0.3:
+    dependencies:
+      is-extglob: 2.1.1
+
+  is-number@7.0.0: {}
+
+  isexe@2.0.0: {}
+
+  isexe@3.1.1: {}
+
+  js-tokens@9.0.0: {}
+
+  js-yaml@4.1.0:
+    dependencies:
+      argparse: 2.0.1
+
+  json-buffer@3.0.1: {}
+
+  json-parse-even-better-errors@4.0.0: {}
+
+  json-schema-traverse@0.4.1: {}
+
+  json-stable-stringify-without-jsonify@1.0.1: {}
+
+  keyv@4.5.4:
+    dependencies:
+      json-buffer: 3.0.1
+
+  levn@0.4.1:
+    dependencies:
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+
+  local-pkg@0.5.0:
+    dependencies:
+      mlly: 1.7.3
+      pkg-types: 1.2.1
+
+  locate-path@6.0.0:
+    dependencies:
+      p-locate: 5.0.0
+
+  lodash-es@4.17.21: {}
+
+  lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
+    dependencies:
+      '@types/lodash-es': 4.17.12
+      lodash: 4.17.21
+      lodash-es: 4.17.21
+
+  lodash.merge@4.6.2: {}
+
+  lodash@4.17.21: {}
+
+  magic-string@0.30.12:
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.0
+
+  memoize-one@6.0.0: {}
+
+  memorystream@0.3.1: {}
+
+  merge2@1.4.1: {}
+
+  micromatch@4.0.8:
+    dependencies:
+      braces: 3.0.3
+      picomatch: 2.3.1
+
+  mime-db@1.52.0: {}
+
+  mime-types@2.1.35:
+    dependencies:
+      mime-db: 1.52.0
+
+  minimatch@3.1.2:
+    dependencies:
+      brace-expansion: 1.1.11
+
+  minimatch@9.0.5:
+    dependencies:
+      brace-expansion: 2.0.1
+
+  mlly@1.7.3:
+    dependencies:
+      acorn: 8.14.0
+      pathe: 1.1.2
+      pkg-types: 1.2.1
+      ufo: 1.5.4
+
+  ms@2.1.3: {}
+
+  muggle-string@0.4.1: {}
+
+  nanoid@3.3.7: {}
+
+  natural-compare@1.4.0: {}
+
+  normalize-path@3.0.0: {}
+
+  normalize-wheel-es@1.2.0: {}
+
+  npm-normalize-package-bin@4.0.0: {}
+
+  npm-run-all2@7.0.1:
+    dependencies:
+      ansi-styles: 6.2.1
+      cross-spawn: 7.0.3
+      memorystream: 0.3.1
+      minimatch: 9.0.5
+      pidtree: 0.6.0
+      read-package-json-fast: 4.0.0
+      shell-quote: 1.8.1
+      which: 5.0.0
+
+  nth-check@2.1.1:
+    dependencies:
+      boolbase: 1.0.0
+
+  optionator@0.9.4:
+    dependencies:
+      deep-is: 0.1.4
+      fast-levenshtein: 2.0.6
+      levn: 0.4.1
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+      word-wrap: 1.2.5
+
+  p-limit@3.1.0:
+    dependencies:
+      yocto-queue: 0.1.0
+
+  p-locate@5.0.0:
+    dependencies:
+      p-limit: 3.1.0
+
+  parent-module@1.0.1:
+    dependencies:
+      callsites: 3.1.0
+
+  path-browserify@1.0.1: {}
+
+  path-exists@4.0.0: {}
+
+  path-key@3.1.1: {}
+
+  pathe@1.1.2: {}
+
+  picocolors@1.1.1: {}
+
+  picomatch@2.3.1: {}
+
+  picomatch@4.0.2: {}
+
+  pidtree@0.6.0: {}
+
+  pinia@2.2.6(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)):
+    dependencies:
+      '@vue/devtools-api': 6.6.4
+      vue: 3.5.12(typescript@5.6.3)
+      vue-demi: 0.14.10(vue@3.5.12(typescript@5.6.3))
+    optionalDependencies:
+      typescript: 5.6.3
+
+  pkg-types@1.2.1:
+    dependencies:
+      confbox: 0.1.8
+      mlly: 1.7.3
+      pathe: 1.1.2
+
+  postcss-selector-parser@6.1.2:
+    dependencies:
+      cssesc: 3.0.0
+      util-deprecate: 1.0.2
+
+  postcss@8.4.47:
+    dependencies:
+      nanoid: 3.3.7
+      picocolors: 1.1.1
+      source-map-js: 1.2.1
+
+  prelude-ls@1.2.1: {}
+
+  prettier-linter-helpers@1.0.0:
+    dependencies:
+      fast-diff: 1.3.0
+
+  prettier@3.3.3: {}
+
+  proxy-from-env@1.1.0: {}
+
+  punycode@2.3.1: {}
+
+  queue-microtask@1.2.3: {}
+
+  read-package-json-fast@4.0.0:
+    dependencies:
+      json-parse-even-better-errors: 4.0.0
+      npm-normalize-package-bin: 4.0.0
+
+  readdirp@3.6.0:
+    dependencies:
+      picomatch: 2.3.1
+
+  resolve-from@4.0.0: {}
+
+  reusify@1.0.4: {}
+
+  rollup@4.24.4:
+    dependencies:
+      '@types/estree': 1.0.6
+    optionalDependencies:
+      '@rollup/rollup-android-arm-eabi': 4.24.4
+      '@rollup/rollup-android-arm64': 4.24.4
+      '@rollup/rollup-darwin-arm64': 4.24.4
+      '@rollup/rollup-darwin-x64': 4.24.4
+      '@rollup/rollup-freebsd-arm64': 4.24.4
+      '@rollup/rollup-freebsd-x64': 4.24.4
+      '@rollup/rollup-linux-arm-gnueabihf': 4.24.4
+      '@rollup/rollup-linux-arm-musleabihf': 4.24.4
+      '@rollup/rollup-linux-arm64-gnu': 4.24.4
+      '@rollup/rollup-linux-arm64-musl': 4.24.4
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.24.4
+      '@rollup/rollup-linux-riscv64-gnu': 4.24.4
+      '@rollup/rollup-linux-s390x-gnu': 4.24.4
+      '@rollup/rollup-linux-x64-gnu': 4.24.4
+      '@rollup/rollup-linux-x64-musl': 4.24.4
+      '@rollup/rollup-win32-arm64-msvc': 4.24.4
+      '@rollup/rollup-win32-ia32-msvc': 4.24.4
+      '@rollup/rollup-win32-x64-msvc': 4.24.4
+      fsevents: 2.3.3
+
+  run-parallel@1.2.0:
+    dependencies:
+      queue-microtask: 1.2.3
+
+  scule@1.3.0: {}
+
+  semver@7.6.3: {}
+
+  shebang-command@2.0.0:
+    dependencies:
+      shebang-regex: 3.0.0
+
+  shebang-regex@3.0.0: {}
+
+  shell-quote@1.8.1: {}
+
+  source-map-js@1.2.1: {}
+
+  strip-json-comments@3.1.1: {}
+
+  strip-literal@2.1.0:
+    dependencies:
+      js-tokens: 9.0.0
+
+  supports-color@7.2.0:
+    dependencies:
+      has-flag: 4.0.0
+
+  synckit@0.9.2:
+    dependencies:
+      '@pkgr/core': 0.1.1
+      tslib: 2.8.1
+
+  text-table@0.2.0: {}
+
+  to-regex-range@5.0.1:
+    dependencies:
+      is-number: 7.0.0
+
+  ts-api-utils@1.4.0(typescript@5.6.3):
+    dependencies:
+      typescript: 5.6.3
+
+  tslib@2.8.1: {}
+
+  type-check@0.4.0:
+    dependencies:
+      prelude-ls: 1.2.1
+
+  type-fest@0.20.2: {}
+
+  typescript-eslint@8.13.0(eslint@9.14.0)(typescript@5.6.3):
+    dependencies:
+      '@typescript-eslint/eslint-plugin': 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
+      '@typescript-eslint/parser': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
+      '@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
+    optionalDependencies:
+      typescript: 5.6.3
+    transitivePeerDependencies:
+      - eslint
+      - supports-color
+
+  typescript@5.6.3: {}
+
+  ufo@1.5.4: {}
+
+  undici-types@6.19.8: {}
+
+  unimport@3.13.1(rollup@4.24.4):
+    dependencies:
+      '@rollup/pluginutils': 5.1.3(rollup@4.24.4)
+      acorn: 8.14.0
+      escape-string-regexp: 5.0.0
+      estree-walker: 3.0.3
+      fast-glob: 3.3.2
+      local-pkg: 0.5.0
+      magic-string: 0.30.12
+      mlly: 1.7.3
+      pathe: 1.1.2
+      pkg-types: 1.2.1
+      scule: 1.3.0
+      strip-literal: 2.1.0
+      unplugin: 1.15.0
+    transitivePeerDependencies:
+      - rollup
+      - webpack-sources
+
+  unplugin-auto-import@0.18.3(@vueuse/core@9.13.0(vue@3.5.12(typescript@5.6.3)))(rollup@4.24.4):
+    dependencies:
+      '@antfu/utils': 0.7.10
+      '@rollup/pluginutils': 5.1.3(rollup@4.24.4)
+      fast-glob: 3.3.2
+      local-pkg: 0.5.0
+      magic-string: 0.30.12
+      minimatch: 9.0.5
+      unimport: 3.13.1(rollup@4.24.4)
+      unplugin: 1.15.0
+    optionalDependencies:
+      '@vueuse/core': 9.13.0(vue@3.5.12(typescript@5.6.3))
+    transitivePeerDependencies:
+      - rollup
+      - webpack-sources
+
+  unplugin-vue-components@0.27.4(@babel/parser@7.26.2)(rollup@4.24.4)(vue@3.5.12(typescript@5.6.3)):
+    dependencies:
+      '@antfu/utils': 0.7.10
+      '@rollup/pluginutils': 5.1.3(rollup@4.24.4)
+      chokidar: 3.6.0
+      debug: 4.3.7
+      fast-glob: 3.3.2
+      local-pkg: 0.5.0
+      magic-string: 0.30.12
+      minimatch: 9.0.5
+      mlly: 1.7.3
+      unplugin: 1.15.0
+      vue: 3.5.12(typescript@5.6.3)
+    optionalDependencies:
+      '@babel/parser': 7.26.2
+    transitivePeerDependencies:
+      - rollup
+      - supports-color
+      - webpack-sources
+
+  unplugin@1.15.0:
+    dependencies:
+      acorn: 8.14.0
+      webpack-virtual-modules: 0.6.2
+
+  uri-js@4.4.1:
+    dependencies:
+      punycode: 2.3.1
+
+  util-deprecate@1.0.2: {}
+
+  vite@5.4.10(@types/node@20.17.6):
+    dependencies:
+      esbuild: 0.21.5
+      postcss: 8.4.47
+      rollup: 4.24.4
+    optionalDependencies:
+      '@types/node': 20.17.6
+      fsevents: 2.3.3
+
+  vscode-uri@3.0.8: {}
+
+  vue-demi@0.14.10(vue@3.5.12(typescript@5.6.3)):
+    dependencies:
+      vue: 3.5.12(typescript@5.6.3)
+
+  vue-eslint-parser@9.4.3(eslint@9.14.0):
+    dependencies:
+      debug: 4.3.7
+      eslint: 9.14.0
+      eslint-scope: 7.2.2
+      eslint-visitor-keys: 3.4.3
+      espree: 9.6.1
+      esquery: 1.6.0
+      lodash: 4.17.21
+      semver: 7.6.3
+    transitivePeerDependencies:
+      - supports-color
+
+  vue-router@4.4.5(vue@3.5.12(typescript@5.6.3)):
+    dependencies:
+      '@vue/devtools-api': 6.6.4
+      vue: 3.5.12(typescript@5.6.3)
+
+  vue-tsc@2.1.10(typescript@5.6.3):
+    dependencies:
+      '@volar/typescript': 2.4.8
+      '@vue/language-core': 2.1.10(typescript@5.6.3)
+      semver: 7.6.3
+      typescript: 5.6.3
+
+  vue@3.5.12(typescript@5.6.3):
+    dependencies:
+      '@vue/compiler-dom': 3.5.12
+      '@vue/compiler-sfc': 3.5.12
+      '@vue/runtime-dom': 3.5.12
+      '@vue/server-renderer': 3.5.12(vue@3.5.12(typescript@5.6.3))
+      '@vue/shared': 3.5.12
+    optionalDependencies:
+      typescript: 5.6.3
+
+  webpack-virtual-modules@0.6.2: {}
+
+  which@2.0.2:
+    dependencies:
+      isexe: 2.0.0
+
+  which@5.0.0:
+    dependencies:
+      isexe: 3.1.1
+
+  word-wrap@1.2.5: {}
+
+  xml-name-validator@4.0.0: {}
+
+  yocto-queue@0.1.0: {}

BIN
public/favicon.ico


+ 9 - 0
src/App.vue

@@ -0,0 +1,9 @@
+<script setup lang="ts">
+import { RouterView } from 'vue-router'
+</script>
+
+<template>
+  <RouterView />
+</template>
+
+<style scoped></style>

+ 269 - 0
src/api/index.ts

@@ -0,0 +1,269 @@
+import instance from '@/tool/axios'
+
+import type { UserData } from '@/types/User'
+import type { Pages, AsProp } from '@/types/Tool'
+import type { IAsSpace } from '@/types/AsSpace'
+import type { ISourceMaterialProps } from '@/types/SourceMaterial'
+import type { CatalogTreeOri } from '@/types/Catalog'
+import type { Advertiser } from '@/types/Advertiser'
+import type { OriginalityItem } from '@/types/AdListPage'
+
+/**
+ * 获取验证码
+ */
+export const getCaptcha = (phone: string) => {
+  return instance.post('/ad/users/sendCond', { phone })
+}
+
+/**
+ * 登录
+ */
+export const login = (data: UserData) => {
+  return instance.post('/ad/users/login', data)
+}
+
+/**
+ * 获取站点列表
+ */
+export const getSiteList = () => {
+  return instance.get('/ad/media/list')
+}
+
+/**
+ * 获取创意列表
+ */
+export const getOriginalityList = (data: Pages) => {
+  return instance.post('/ad/creatives/list', data)
+}
+
+/**
+ * 获取创意详情
+ */
+export const getOriginalityDetail = (id: number) => {
+  return instance.get(`/ad/creatives/info/${id}`)
+}
+
+/**
+ * 删除创意
+ */
+export const deleteOriginality = (id: number) => {
+  return instance.post(`/ad/creatives/delete/${id}`)
+}
+
+/**
+ * 创建创意
+ */
+export const createOriginality = (data: OriginalityItem) => {
+  return instance.post('/ad/creatives/create', data)
+}
+
+/**
+ * 修改创意
+ */
+export const updateOriginality = (data: OriginalityItem) => {
+  return instance.post('/ad/creatives/update', data)
+}
+
+/**
+ * 修改广告创意状态
+ */
+export const updateOriginalityStatus = (data: { id: number; status: number }) => {
+  return instance.post('/ad/creatives/updateStatus', data)
+}
+
+/**
+ * 获取可选广告位列表
+ */
+export const getAsList = (data: AsProp) => {
+  return instance.post('/ad/slots/selectable', data)
+}
+
+/**
+ * 获取广告位列表
+ */
+export const getAsSpaceList = (data: Pages) => {
+  return instance.post('/ad/slots/list', data)
+}
+
+/**
+ * 创建广告位列表
+ */
+export const createAsSpace = (data: IAsSpace) => {
+  return instance.post('/ad/slots/create', data)
+}
+
+/**
+ * 删除广告位
+ */
+export const deleteAsSpace = (id: number) => {
+  return instance.delete(`/ad/slots/delete/${id}`)
+}
+
+/**
+ * 修改广告位
+ */
+export const updateAsSpace = (data: IAsSpace) => {
+  return instance.put('/ad/slots/update', data)
+}
+
+/**
+ * 获取栏目树
+ */
+export const getAsTree = (media: number) => {
+  return instance.get('/ad/catalog/tree?mediaId=' + media)
+}
+
+/**
+ * 删除栏目
+ */
+export const deleteAsTree = (id: number) => {
+  return instance.delete(`/ad/catalog/delete/${id}`)
+}
+
+/**
+ * 修改栏目
+ */
+export const updateAsTree = (data: CatalogTreeOri) => {
+  return instance.put('/ad/catalog/update', data)
+}
+
+/**
+ * 创建栏目
+ */
+export const createAsTree = (data: CatalogTreeOri) => {
+  return instance.post('/ad/catalog/create', data)
+}
+
+/**
+ * 获取素材列表
+ */
+export const getMaterialList = (data: Pages) => {
+  return instance.post('/ad/stuffs/list', data)
+}
+
+/**
+ * 获取素材详情
+ */
+export const getMaterialDetail = (id: number) => {
+  return instance.get(`/ad/stuffs/info/${id}`)
+}
+
+/**
+ * 创建素材
+ */
+export const createMaterial = (data: ISourceMaterialProps) => {
+  return instance.post('/ad/stuffs/create', data)
+}
+
+/**
+ * 删除素材
+ */
+export const deleteMaterial = (id: number) => {
+  return instance.delete(`/ad/stuffs/delete/${id}`)
+}
+
+/**
+ * 上传素材
+ */
+export const uploadMaterial = (data: FormData) => {
+  return instance.post('/ad/stuffs/upload', data, {
+    headers: {
+      'Content-Type': 'multipart/form-data', // 用于文件上传
+    },
+  })
+}
+
+/**
+ * 修改素材
+ */
+export const updateMaterial = (data: ISourceMaterialProps) => {
+  return instance.put('/ad/stuffs/update', data)
+}
+
+/**
+ * 素材启用禁用
+ */
+export const updateMaterialStatus = (data: { id: number; status: number }) => {
+  return instance.post('/ad/stuffs/updateStatus', data)
+}
+
+/**
+ * 广告主列表
+ */
+export const getAdvertiserList = (data: Pages) => {
+  return instance.post('/ad/advertiser/list', data)
+}
+
+/**
+ * 广告主创建
+ */
+export const createAdvertiser = (data: Advertiser) => {
+  return instance.post('/ad/advertiser/create', data)
+}
+
+/**
+ * 广告主更新
+ */
+export const updateAdvertiser = (data: Advertiser) => {
+  return instance.post('/ad/advertiser/update', data)
+}
+
+/**
+ * 广告主删除
+ */
+export const deleteAdvertiser = (advertiserId: number) => {
+  return instance.post(`/ad/advertiser/delete/${advertiserId}`)
+}
+
+/**
+ * 广告主启禁用
+ */
+export const updateAdvertiserStatus = (data: { id: number; status: number }) => {
+  return instance.post('/ad/advertiser/updateStatus', data)
+}
+
+// {
+//   "creativeIds": [1013], 创意id
+//   "stuffIds": [1008], 素材id
+//   "startDate": "2024-11-01",
+//   "endDate": "2024-12-30",
+//   "provinces": ["陕西省"],
+//   "cities": ["西安市"],
+//   "pageNum": 1,
+//   "pageSize": 10
+// }
+
+/**
+ * 广告主日报表
+ */
+export const getAdvertiserReport = (data: Pages) => {
+  return instance.post('/ad/report/adv-day', data)
+}
+
+/**
+ * 广告主月报表
+ */
+export const getAdvertiserMonthReport = (data: Pages) => {
+  return instance.post('/ad/report/adv-month', data)
+}
+
+/**
+ * 媒体日报表
+ */
+export const getMediaReport = (data: Pages) => {
+  return instance.post('/ad/report/media-day', data)
+}
+
+/**
+ * 媒体月报表
+ */
+export const getMediaMonthReport = (data: Pages) => {
+  return instance.post('/ad/report/media-month', data)
+}
+
+/**
+ * 省市
+ */
+export const getProvinceCity = () => {
+  return instance.get('http://cxzx.smcic.net/ad/config/cn.json')
+}

+ 86 - 0
src/assets/base.css

@@ -0,0 +1,86 @@
+/* color palette from <https://github.com/vuejs/theme> */
+:root {
+  --vt-c-white: #ffffff;
+  --vt-c-white-soft: #f8f8f8;
+  --vt-c-white-mute: #f2f2f2;
+
+  --vt-c-black: #181818;
+  --vt-c-black-soft: #222222;
+  --vt-c-black-mute: #282828;
+
+  --vt-c-indigo: #2c3e50;
+
+  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
+  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
+  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
+  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
+
+  --vt-c-text-light-1: var(--vt-c-indigo);
+  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
+  --vt-c-text-dark-1: var(--vt-c-white);
+  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
+}
+
+/* semantic color variables for this project */
+:root {
+  --color-background: var(--vt-c-white);
+  --color-background-soft: var(--vt-c-white-soft);
+  --color-background-mute: var(--vt-c-white-mute);
+
+  --color-border: var(--vt-c-divider-light-2);
+  --color-border-hover: var(--vt-c-divider-light-1);
+
+  --color-heading: var(--vt-c-text-light-1);
+  --color-text: var(--vt-c-text-light-1);
+
+  --section-gap: 160px;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --color-background: var(--vt-c-black);
+    --color-background-soft: var(--vt-c-black-soft);
+    --color-background-mute: var(--vt-c-black-mute);
+
+    --color-border: var(--vt-c-divider-dark-2);
+    --color-border-hover: var(--vt-c-divider-dark-1);
+
+    --color-heading: var(--vt-c-text-dark-1);
+    --color-text: var(--vt-c-text-dark-2);
+  }
+}
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+  margin: 0;
+  font-weight: normal;
+}
+
+body {
+  height: 100vh;
+  color: var(--color-text);
+  background: var(--color-background);
+  transition:
+    color 0.5s,
+    background-color 0.5s;
+  line-height: 1.6;
+  font-family:
+    Inter,
+    -apple-system,
+    BlinkMacSystemFont,
+    'Segoe UI',
+    Roboto,
+    Oxygen,
+    Ubuntu,
+    Cantarell,
+    'Fira Sans',
+    'Droid Sans',
+    'Helvetica Neue',
+    sans-serif;
+  font-size: 15px;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}

+ 29 - 0
src/assets/main.css

@@ -0,0 +1,29 @@
+@import './base.css';
+
+#app {
+  max-width: 1280px;
+  font-weight: normal;
+}
+
+a,
+.green {
+  text-decoration: none;
+  color: hsla(160, 100%, 37%, 1);
+  transition: 0.4s;
+  padding: 3px;
+}
+
+.main {
+  width: 100vw;
+  height: 100vh;
+  overflow: hidden;
+}
+
+@media (hover: hover) {
+  a:hover {
+    background-color: hsla(160, 100%, 37%, 0.2);
+  }
+}
+
+@media (min-width: 1024px) {
+}

+ 191 - 0
src/components/mediaComponents.vue

@@ -0,0 +1,191 @@
+<template>
+  <el-button type="primary" :loading="loading" v-if="!src">
+    <label class="file" for="file"> 上传素材 </label>
+    <input
+      style="display: none"
+      @change="chagne"
+      type="file"
+      name="file"
+      accept="image/*,video/*"
+      id="file"
+    />
+  </el-button>
+  <!-- 文件上传 图片 视频 -->
+  <div v-else class="media-components">
+    <svg
+      t="1732589883711"
+      class="close"
+      viewBox="0 0 1024 1024"
+      version="1.1"
+      xmlns="http://www.w3.org/2000/svg"
+      @click="close"
+      width="16"
+      height="16"
+    >
+      <path
+        d="M548.992 503.744L885.44 167.328a31.968 31.968 0 1 0-45.248-45.248L503.744 458.496 167.328 122.08a31.968 31.968 0 1 0-45.248 45.248l336.416 336.416L122.08 840.16a31.968 31.968 0 1 0 45.248 45.248l336.416-336.416L840.16 885.44a31.968 31.968 0 1 0 45.248-45.248L548.992 503.744z"
+        p-id="4267"
+      />
+    </svg>
+    <el-image style="width: 120px; height: 120px" :src="src" fit="contain" />
+    <svg
+      t="1732603337032"
+      v-if="fileType === 'video'"
+      class="play"
+      viewBox="0 0 1024 1024"
+      version="1.1"
+      xmlns="http://www.w3.org/2000/svg"
+      p-id="5375"
+      width="32"
+      height="32"
+      @click="playVideo"
+    >
+      <path
+        d="M512 0C230.4 0 0 230.4 0 512s230.4 512 512 512 512-230.4 512-512S793.6 0 512 0z m0 981.333333C253.866667 981.333333 42.666667 770.133333 42.666667 512S253.866667 42.666667 512 42.666667s469.333333 211.2 469.333333 469.333333-211.2 469.333333-469.333333 469.333333z"
+        fill="#707070"
+        p-id="5376"
+      ></path>
+      <path
+        d="M672 441.6l-170.666667-113.066667c-57.6-38.4-106.666667-12.8-106.666666 57.6v256c0 70.4 46.933333 96 106.666666 57.6l170.666667-113.066666c57.6-42.666667 57.6-106.666667 0-145.066667z"
+        fill="#707070"
+        p-id="5377"
+      ></path>
+    </svg>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { uploadMaterial } from '@/api/index'
+// 获取组件传参
+import { ElMessage } from 'element-plus'
+import { defineProps, ref } from 'vue'
+// 传参父组件
+const emit = defineEmits(['change', 'getCalc'])
+const props = defineProps<{ src: string }>()
+const loading = ref(false)
+const fileType = ref('')
+const src = ref(props.src)
+
+let oriUrl = ''
+let times = 0
+
+const close = () => {
+  src.value = ''
+  fileType.value = ''
+  oriUrl = ''
+  emit('getCalc', 0, 0)
+  emit('change', '')
+}
+
+// 传参为文件
+const chagne = (e: Event) => {
+  if (!e.target) return
+  const inputElement = e.target as HTMLInputElement
+  if (!inputElement.files || !inputElement.files.length) return
+  loading.value = true
+  // 上传文件获取地址后传出组件
+  const file: File = inputElement.files[0]
+  const formData = new FormData()
+  formData.append('file', file)
+  uploadMaterial(formData)
+    .then(({ data }) => {
+      ElMessage({
+        message: '上传成功',
+        type: 'success',
+      })
+      getImage(file)
+      inputElement.value = ''
+      times = 0
+      oriUrl = data.url
+      emit('change', oriUrl)
+      loading.value = false
+    })
+    .catch(() => {
+      if (times++ < 5) {
+        ElMessage({
+          message: '正在重新上传',
+          type: 'info',
+        })
+        const t = setTimeout(function () {
+          console.log(times)
+          clearTimeout(t)
+          chagne(e)
+        }, 1000)
+      } else {
+        ElMessage({
+          message: '网络有问题,请检查后重新上传',
+          type: 'info',
+        })
+        inputElement.value = ''
+        loading.value = false
+        times = 0
+      }
+    })
+}
+
+// 判定如果是图片则直接输出图片,如果是视频则输出第一帧图片
+const getImage = (file: File) => {
+  if (file.type.indexOf('image') !== -1) {
+    src.value = URL.createObjectURL(file)
+    fileType.value = 'image'
+    const img = new Image()
+    img.onload = function () {
+      emit('getCalc', img.width, img.height)
+    }
+    img.src = URL.createObjectURL(file)
+    return
+  }
+  fileType.value = 'video'
+  // 视频 获取第一帧
+  const video = document.createElement('video')
+  // 播放本地视频文件
+  video.src = URL.createObjectURL(file)
+  video.onloadeddata = function () {
+    // 跳转到第二秒
+    video.currentTime = 2
+    // 当视频到达第二秒时,绘制帧
+    video.addEventListener('seeked', () => {
+      // 获取第一帧
+      emit('getCalc', video.videoWidth, video.videoHeight)
+      const canvas = document.createElement('canvas')
+      canvas.width = video.videoWidth
+      canvas.height = video.videoHeight
+      const ctx = canvas.getContext('2d')
+      ctx?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
+      src.value = canvas.toDataURL()
+    })
+  }
+}
+
+const playVideo = () => {
+  window.open(oriUrl)
+}
+</script>
+
+<style scoped>
+.media-components {
+  border-radius: 4px;
+  background: #fdfdfd;
+  width: 120px;
+  height: 120px;
+  box-shadow: var(--el-box-shadow-lighter);
+  position: relative;
+}
+
+.close {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  cursor: pointer;
+  z-index: 10;
+}
+
+.play {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  z-index: 10;
+  cursor: pointer;
+}
+</style>

+ 24 - 0
src/main.ts

@@ -0,0 +1,24 @@
+import './assets/main.css'
+// 导入element-plus的css
+import 'element-plus/dist/index.css'
+
+import ElementPlus from 'element-plus'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+
+import App from './App.vue'
+import router from './router'
+
+// import { ad_main } from './tool/adList'
+// ad_main()
+
+const app = createApp(App)
+app.use(ElementPlus, {
+  locale: zhCn,
+})
+app.use(createPinia())
+app.use(router)
+
+app.mount('#app')

+ 80 - 0
src/router/index.ts

@@ -0,0 +1,80 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+import SkeletonPage from '@/views/SkeletonPage.vue'
+
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: '/login',
+      name: 'login',
+      component: () => import('@/views/LoginPage.vue'),
+    },
+    {
+      path: '/authorized',
+      name: 'Authorized',
+      component: SkeletonPage,
+      children: [
+        {
+          path: 'adlist',
+          name: 'AdList',
+          component: () => import('../views/AdListPage.vue'),
+        },
+        {
+          path: 'originality',
+          name: 'Originality',
+          component: () => import('../views/OriginalityPage.vue'),
+        },
+        {
+          path: 'advertiser',
+          name: 'Advertiser',
+          component: () => import('../views/AdvertiserPage.vue'),
+        },
+        {
+          path: 'sourceMaterial',
+          name: 'SourceMaterial',
+          component: () => import('../views/SourceMaterialPage.vue'),
+        },
+        {
+          path: 'site',
+          name: 'Site',
+          component: () => import('../views/SitePage.vue'),
+        },
+        {
+          path: 'advertisingSpace',
+          name: 'AdvertisingSpace',
+          component: () => import('../views/AdvertisingSpacePage.vue'),
+        },
+        {
+          path: 'advertisingSpaceDate',
+          name: 'AdvertisingSpaceDate',
+          component: () => import('../views/AdvertisingSpaceDatePage.vue'),
+        },
+        {
+          path: 'reportForms',
+          name: 'ReportForms',
+          component: () => import('../views/ReportFormsPage.vue'),
+        },
+        {
+          path: 'reportSpaceForms',
+          name: 'ReportSpaceForms',
+          component: () => import('../views/ReportSpaceFormsPage.vue'),
+        },
+      ],
+    },
+  ],
+})
+// 路由卫士
+router.beforeEach((to, from, next) => {
+  const token = localStorage.getItem('token')
+  // 如果是登录页面,直接放行
+  if (to.path === '/') {
+    return next(token ? '/authorized/originality' : '/login')
+  }
+  if (to.path === '/login') return next()
+  // 如果不是登录页面,判断是否登录,如果登录,直接放行,否则跳转到登录页面
+  if (token) return next()
+  next('/login')
+})
+
+export default router

+ 12 - 0
src/stores/counter.ts

@@ -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 }
+})

+ 138 - 0
src/tool/adList.ts

@@ -0,0 +1,138 @@
+import type { AdMainConfigList } from '@/types/Tool'
+
+/**
+ * 创建广告
+ * config 参数说明
+ * @param {string[]} ad_id 广告id
+ * @param {string} ad_type 广告类型 1:视频 2:图片
+ * @param {string} ad_pos 广告位置
+ * @param {string[]} ad_size 广告尺寸
+ * @param {string} ad_text 广告文案
+ * @param {string[]} ad_url 广告链接
+ * @param {string[]} ad_cover 广告图片
+ */
+export const ad_main = function (config: AdMainConfigList) {
+  // 广告sdk
+  if (localStorage.getItem('ad_id')) {
+    ad_init(config)
+    return
+  }
+
+  // 生成并输出浏览器指纹
+  generateBrowserFingerprint().then((ad_id) => {
+    localStorage.setItem('ad_id', ad_id + '')
+    ad_init(config)
+  })
+}
+
+function ad_init(config: AdMainConfigList) {
+  for (let i = 0; i < config.length; i++) {
+    const v = config[i]
+    // 获取对应广告位置
+    const ad_pos = document.querySelector('#' + v.ad_pos)
+    if (!ad_pos) return console.error('广告位置不存在:' + v.ad_pos)
+    const ele = document.createElement('div')
+    ele.style.width = v.ad_size[0] + 'px'
+    ele.style.height = v.ad_size[1] + 'px'
+    // 添加对应广告类型
+    switch (v.ad_type) {
+      case 1:
+        // 广告视频
+        ele.innerHTML += `<video src="${v.ad_url}" style="width:100%;height:100%;" />`
+        break
+      case 2:
+        // 广告图片
+        ele.innerHTML += `<img src="${v.ad_cover}" style="width:100%;height:100%;" />`
+        break
+    }
+    // 添加广告文案
+    const ad_text = document.createElement('div')
+    ad_text.innerText = v.ad_text
+    ad_text.style.position = 'absolute'
+    ad_text.style.bottom = '10px'
+    ad_text.style.left = '10px'
+    ad_text.style.color = '#fff'
+    ad_text.style.fontSize = '14px'
+    ele.appendChild(ad_text)
+    ele.addEventListener('click', () => {
+      // 点击广告
+      window.open(v.ad_url)
+    })
+    ad_pos.appendChild(ele)
+  }
+}
+
+function generateBrowserFingerprint() {
+  return new Promise((resolve, reject) => {
+    // 在这里执行获取指纹的操作,例如使用Canvas、WebGL等方法
+    // 然后将生成的指纹字符串作为参数传递给resolve函数
+    // 获取浏览器信息
+    const browserInfo = `${navigator.userAgent} ${window.screen.width}x${window.screen.height}`
+    // 获取时区偏移量
+    const timezoneOffset = Intl.DateTimeFormat().resolvedOptions().timeZone
+    // 在这里处理获取到的指纹信息
+    const canvasFingerprint = getCanvasFingerprint()
+    const webglFingerprint = getWebglFingerprint()
+    // 将所有信息组合成一个字符串
+    const fingerprintStr = `${browserInfo}|${timezoneOffset}|${canvasFingerprint}|${webglFingerprint}`
+    // 使用createHash生成哈希值
+    createHash(fingerprintStr)
+      .then((hash) => {
+        resolve(hash)
+      })
+      .catch((error) => {
+        reject(error)
+      })
+  })
+}
+
+// Canvas指纹获取函数
+function getCanvasFingerprint() {
+  const canvas = document.createElement('canvas')
+  const ctx = canvas.getContext('2d')
+  if (!ctx) return ''
+  // 绘制一些内容
+  ctx.fillStyle = '#f60'
+  ctx.fillRect(0, 0, 16, 16)
+  ctx.fillStyle = '#069'
+  ctx.font = '11pt Arial'
+  ctx.fillText('Cwm fjord!!', 4, 10)
+  ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'
+  ctx.fillText('Cwm fjord!!', 5, 11)
+  ctx.scale(2, 2)
+  ctx.fillStyle = '#fff'
+  ctx.font = '24pt Arial'
+  ctx.fillText('Cwm fjord!!', 10, 25)
+  ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'
+  ctx.fillText('Cwm fjord!!', 10, 25)
+
+  const data = canvas.toDataURL()
+  return data.replace(/data:image\/png;base64,/, '')
+}
+
+// WebGL指纹获取函数
+function getWebglFingerprint() {
+  const canvas = document.createElement('canvas')
+  const gl = (canvas.getContext('webgl') ||
+    canvas.getContext('experimental-webgl')) as WebGLRenderingContext
+  if (!gl) return ''
+  const extensions = gl.getSupportedExtensions()
+  if (!extensions) return ''
+  const extStr = extensions.join(',')
+  const rendererInfo = gl.getParameter(gl.RENDERER) + '/' + gl.getParameter(gl.VENDOR)
+  return rendererInfo + extStr
+}
+
+async function createHash(str: string) {
+  // 将字符串转换为 Uint8Array
+  const encoder = new TextEncoder()
+  const data = encoder.encode(str)
+
+  // 使用 SubtleCrypto API 计算 SHA-256 摘要
+  const hashBuffer = await crypto.subtle.digest('SHA-256', data)
+
+  // 将 ArrayBuffer 转换为十六进制字符串
+  const hashArray = Array.from(new Uint8Array(hashBuffer))
+  const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('')
+  return hashHex
+}

+ 61 - 0
src/tool/axios.ts

@@ -0,0 +1,61 @@
+import axios from 'axios'
+import { ElMessage } from 'element-plus'
+import { useRouter } from 'vue-router'
+const router = useRouter()
+
+const instance = axios.create({
+  baseURL: 'http://10.30.160.129:9002/',
+  timeout: 10000,
+})
+
+// 请求拦截器
+instance.interceptors.request.use(
+  (config) => {
+    // 添加头部token
+    const token = localStorage.getItem('token')
+    if (token) {
+      config.headers.Authorization = token
+    }
+    return config
+  },
+  (error) => {
+    return Promise.reject(error)
+  },
+)
+
+// 响应拦截器
+instance.interceptors.response.use(
+  (response) => {
+    if (response.status !== 200) {
+      ElMessage.error('请求失败')
+      return Promise.reject(new Error('请求失败'))
+    }
+    const {
+      data: { code },
+    } = response
+    if (code !== 0) {
+      if (code === 40001) {
+        localStorage.removeItem('token')
+        ElMessage.error('登录过期,请重新登录')
+        router.replace('/login')
+        return Promise.reject(new Error('登录过期,请重新登录'))
+      }
+      ElMessage.error(response.data.message)
+      return Promise.reject(new Error(response.data.message))
+    }
+    return response.data
+  },
+  (error) => {
+    let msg = '请求失败'
+    if (error.response && error.response.data && error.response.data.message) {
+      msg = error.response.data.message
+    }
+    if (error.status !== 200) {
+      ElMessage.error(msg)
+      return Promise.reject(new Error(msg))
+    }
+    return Promise.reject(error)
+  },
+)
+
+export default instance

+ 64 - 0
src/tool/index.ts

@@ -0,0 +1,64 @@
+import type { DateObject } from '@/types/Tool'
+
+export function formatDate(date: number | string): DateObject {
+  const dateFmt = new Date(date)
+  // 获取年月日时分秒
+  const fmtYear = dateFmt.getFullYear()
+  const month = dateFmt.getMonth() + 1
+  const day = dateFmt.getDate()
+  const hour = dateFmt.getHours()
+  const minute = dateFmt.getMinutes()
+  const second = dateFmt.getSeconds()
+  const week = dateFmt.getDay()
+  const weekArr = ['日', '一', '二', '三', '四', '五', '六']
+  const weekStr = '星期' + weekArr[week]
+  // 格式化
+  const fmtMonth = month < 10 ? '0' + month : month
+  const fmtDay = day < 10 ? '0' + day : day
+  const fmtHour = hour < 10 ? '0' + hour : hour
+  const fmtMinute = minute < 10 ? '0' + minute : minute
+  const fmtSecond = second < 10 ? '0' + second : second
+  return {
+    fmtYear,
+    fmtMonth,
+    fmtDay,
+    fmtHour,
+    fmtMinute,
+    fmtSecond,
+    weekStr,
+  }
+}
+
+export function formatDateSite(data: number | string, type: string): string {
+  const D: DateObject = formatDate(data)
+  switch (type) {
+    case 'Y-M-D':
+      return `${D.fmtYear}-${D.fmtMonth}-${D.fmtDay}`
+    case 'Y-M-D H:I:S':
+      return `${D.fmtYear}-${D.fmtMonth}-${D.fmtDay} ${D.fmtHour}:${D.fmtMinute}:${D.fmtSecond}`
+    default:
+      return ''
+  }
+}
+
+export const statusList = [
+  { label: '全部', value: -1 },
+  { label: '待审核', value: 0 },
+  { label: '审核不通过', value: 3 },
+  { label: '审核通过', value: 4 },
+  { label: '已删除', value: 5 },
+  { label: '下线', value: 6 },
+  { label: '启用', value: 10 },
+  { label: '过期', value: 7 },
+  { label: '暂停', value: 8 },
+]
+
+export function getStatus(status: number) {
+  let statusText = ''
+  statusList.forEach((item) => {
+    if (item.value === status) {
+      statusText = item.label
+    }
+  })
+  return statusText
+}

+ 84 - 0
src/types/AdListPage.d.ts

@@ -0,0 +1,84 @@
+// 生成一个对象类型
+export type AdListItem = {
+  start_date: string
+  end_date: string
+  name: string
+}
+
+export type AdList = AdListItem[]
+
+// 媒体列表
+export interface MediaItem {
+  createdTime: string
+  creativeId: number
+  isDefault: number
+  mediaId: number
+  mediaName: string
+  phone: string
+  status: number
+  uid: string
+  updatedTime: string
+  url: string
+}
+
+// 广告位
+export interface SlotsItem {
+  adType: number
+  catalogId: number
+  createdTime: string
+  creativeId: number
+  height: number
+  mediaId: number
+  showSys: null
+  showType: number
+  slotId: number
+  slotName: string
+  status: number
+  terminalType: number
+  uid: number
+  updatedTime: string
+  width: number
+}
+
+// 创意列表
+export interface OriginalityItem {
+  createdTime?: string
+  intervalType?: number
+  creativeId?: number
+  creativeName?: string
+  endDate?: string
+  advertiserId?: number
+  mediaList?: MediaItem[] | number[] | number
+  planId?: number
+  slotsList?: SlotsItem[]
+  startDate?: string
+  status?: number
+  statusDesc?: string
+  timeInterval?: string
+  uid?: number
+  updatedTime?: string
+  dates?: string[]
+  slot?: number[]
+  type?: number
+  showType?: number
+  mediaIds?: number[]
+  slotIds?: number[]
+}
+export type OriginalityList = OriginalityItem[]
+
+// 时间段
+export interface TimeInterval {
+  [key: number]: boolean
+  weekName: string
+  selectAll: boolean
+}
+export type TimeIntervalList = TimeInterval[]
+
+// 广告主
+export interface AdvertiserItem {
+  advertiserId: number
+  enterpriseName: string
+  phone: string
+  status: number
+  uid: number
+}

+ 17 - 0
src/types/Advertiser.d.ts

@@ -0,0 +1,17 @@
+export interface Advertiser {
+  adUserType?: number
+  advertiserId?: number
+  authType?: number
+  createTime?: string
+  enterpriseAddress?: string
+  enterpriseBrand?: string
+  enterpriseContacts?: string
+  enterpriseName?: string
+  enterpriseNationality?: string
+  enterpriseTel?: string
+  licenseExpire?: string
+  licenseId?: string
+  organizationCode?: string
+  status?: number
+  updateTime?: string
+}

+ 68 - 0
src/types/AsSpace.d.ts

@@ -0,0 +1,68 @@
+export interface IAsSpace {
+  mediaId?: number
+  catalogId?: number
+  slotName?: string
+  width?: number
+  height?: number
+  adType?: number
+  slotId?: number
+}
+
+export interface CatalogTreeOri {
+  catalogId: number
+  catalogName: string
+  children: CatalogTreeOri[]
+  createTime: string
+  media: string
+  mediaId: number
+  parentId: number
+  slots: number
+  status: number
+}
+
+export interface CatalogTree {
+  value: number
+  label: string
+  children?: CatalogTree[]
+}
+
+export interface CreativesList {
+  createdTime: string
+  creativeId: number
+  creativeName: string
+  endDate: string
+  intervalType: number
+  mediaList: number[] | null
+  planId: number | null
+  showType: string | null
+  slotsList: number[] | null
+  startDate: string
+  status: number
+  statusDesc: string | null
+  timeInterval: string
+  uid: number | null
+  updatedTime: string | null
+}
+
+export interface AsSpaceItem {
+  adType: number
+  catalogId: number
+  createdTime: string
+  creativesList: CreativesList[]
+  height: number
+  mediaId: number
+  showSys: null
+  slotId: number
+  slotName: string
+  status: number
+  terminalType: null
+  uid: number
+  updatedTime: null
+  width: number
+}
+
+export interface AsSpaceList {
+  name: string
+  dates: string[][]
+  datesList: string[]
+}

+ 12 - 0
src/types/Catalog.d.ts

@@ -0,0 +1,12 @@
+export interface CatalogTreeOri {
+  catalogId?: number
+  catalogName?: string
+  children?: CatalogTreeOri[]
+  createTime?: string
+  media?: string
+  mediaId?: number
+  parentId?: number
+  slots?: null
+  status?: number
+  isEdit?: boolean
+}

+ 7 - 0
src/types/Report.d.ts

@@ -0,0 +1,7 @@
+export interface IReportItem {
+  clickCount: number
+  exposureCount: number
+  requestCount: number
+  sdate: string
+  uvCount: number
+}

+ 18 - 0
src/types/Site.d.ts

@@ -0,0 +1,18 @@
+export interface SiteItem {
+  contacts?: string
+  createdTime?: string
+  description?: null
+  email?: string
+  keyId?: string | null
+  mediaId?: number
+  mediaName?: string
+  mediaType?: number
+  phone?: string
+  status?: number
+  token?: string | null
+  uid?: number
+  updatedTime?: string
+  url?: string
+}
+
+export type SiteList = SiteItem[]

+ 31 - 0
src/types/SourceMaterial.d.ts

@@ -0,0 +1,31 @@
+export interface SourceMaterialItem {
+  addr: string
+  createdTime: string
+  creativeId: number
+  height: number
+  landingPage: string
+  status: number
+  stuffId: number
+  stuffName: string
+  type: number
+  uid: number
+  updatedTime: string
+  width: number
+}
+
+export type SourceMaterialList = SourceMaterialItem[]
+
+export interface ISourceMaterialProps {
+  creativeId?: number
+  stuffName?: string
+  addr?: string
+  landingPage?: string
+  width?: number
+  height?: number
+  type?: number
+  createdTime?: string
+  status?: number
+  stuffId?: number
+  uid?: number
+  updatedTime?: string
+}

+ 53 - 0
src/types/Tool.d.ts

@@ -0,0 +1,53 @@
+import type { MediaItem } from './AdListPage'
+export type AdMainConfig = {
+  ad_pos: string
+  ad_size: number[]
+  ad_url: string
+  ad_type: number
+  ad_cover: string
+  ad_text: string
+}
+
+export type AdMainConfigList = AdMainConfig[]
+
+export interface DateObject {
+  fmtYear: string | number
+  fmtMonth: string | number
+  fmtDay: string | number
+  fmtHour: string | number
+  fmtMinute: string | number
+  fmtSecond: string | number
+  weekStr: string
+}
+
+export interface Pages {
+  pageNum: number
+  pageSize: number
+  total?: number
+  mediaId?: number | null
+  catalogId?: number[]
+  keyword?: string | null
+  status?: number // 未知状态
+  mediaIds?: number[] | null
+  creativeId?: number | null
+  advertiserId?: number | null
+  startDate?: string
+  endDate?: string
+  datas?: string[]
+  cities?: string[]
+  provinces?: string[]
+  province?: string
+  stuffIds?: number[]
+  catalogIds?: number[]
+  advertiserIds?: number[]
+  creativeIds?: number[]
+  type?: string
+}
+
+export interface AsProp {
+  mediaIds: MediaItem[] | number[] | number
+  startDate: string
+  endDate: string
+  intervalType: number | undefined
+  timeInterval: string
+}

+ 5 - 0
src/types/User.d.ts

@@ -0,0 +1,5 @@
+export interface UserData {
+  phone?: string
+  code?: string
+  token?: string
+}

+ 99 - 0
src/views/AdListPage.vue

@@ -0,0 +1,99 @@
+<template>
+  <el-form :inline="true">
+    <el-form-item label="计划名称">
+      <el-input v-model="search" placeholder="输入计划关键词" clearable />
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" @click="onSubmit">搜索</el-button>
+      <el-button type="primary" link @click="creatEditPlan">创建计划</el-button>
+    </el-form-item>
+  </el-form>
+
+  <el-table :data="tableData" stripe style="width: 100%">
+    <el-table-column prop="name" label="名称" />
+    <el-table-column prop="start_date" label="生效日期" />
+    <el-table-column prop="end_date" label="结束日期" />
+    <el-table-column label="操作">
+      <template #default="scope">
+        <el-popconfirm title="确定删除该广告吗?">
+          <template #reference>
+            <el-button link type="primary" size="small">删除</el-button>
+          </template>
+        </el-popconfirm>
+        <el-button link type="primary" size="small" @click="() => creatEditPlan(scope.row)">
+          修改
+        </el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+  <br />
+  <el-pagination
+    background
+    layout="prev, pager, next"
+    :total="tableData.length"
+    @current-change="page"
+  />
+
+  <el-dialog v-model="dialogVisible" title="计划">
+    <el-form :model="form" label-width="auto" style="max-width: 600px">
+      <el-form-item label="计划名称">
+        <el-input v-model="form.name" />
+      </el-form-item>
+      <el-form-item label="计划周期">
+        <el-date-picker
+          v-model="form.dates"
+          type="daterange"
+          format="YYYY-MM-DD"
+          value-format="YYYY-MM-DD"
+          range-separator="到"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="dialogVisible = false">确定</el-button>
+        <el-button type="primary" @click="dialogVisible = false"> 取消 </el-button>
+      </el-form-item>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import type { AdList, AdListItem } from '@/types/AdListPage'
+import { ref } from 'vue'
+
+const search = ref('')
+const dialogVisible = ref(false)
+const form = ref({
+  name: '',
+  dates: ['', ''],
+})
+
+const tableData: AdList = [
+  {
+    start_date: '2016-05-03',
+    end_date: '2016-05-06',
+    name: '咸阳国际机场广告',
+  },
+]
+
+const onSubmit = () => {
+  console.log('submit!')
+}
+
+const page = (val: number) => {
+  console.log(val)
+}
+
+const creatEditPlan = (data: AdListItem) => {
+  if (data != undefined) {
+    form.value = {
+      name: data.name,
+      dates: [data.start_date, data.end_date],
+    }
+  }
+  dialogVisible.value = !dialogVisible.value
+}
+</script>
+
+<style></style>

+ 195 - 0
src/views/AdvertiserPage.vue

@@ -0,0 +1,195 @@
+<!-- 广告主 -->
+<template>
+  <el-form :inline="true">
+    <el-form-item label="广告主名称">
+      <el-input v-model="pages.keyword" placeholder="输入广告主关键词" clearable />
+    </el-form-item>
+    <el-form-item label="状态">
+      <el-select filterable clearable v-model="pages.status" style="width: 200px">
+        <el-option
+          v-for="item in statusList"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" @click="onSubmit">搜索</el-button>
+      <el-button type="primary" link @click="() => creatEditPlan()">创建广告主</el-button>
+    </el-form-item>
+  </el-form>
+
+  <el-table :data="tableData" stripe style="width: 100%">
+    <el-table-column prop="enterpriseName" label="企业" />
+    <el-table-column prop="enterpriseBrand" label="品牌" />
+    <el-table-column prop="enterpriseContacts" label="联系人" />
+    <el-table-column prop="enterpriseTel" label="联系电话" />
+    <el-table-column label="状态" width="60">
+      <template #default="scope">
+        <el-tag v-if="scope.row.status == 10" type="success">启用</el-tag>
+        <el-tag v-if="scope.row.status == 6" type="danger">禁用</el-tag>
+      </template>
+    </el-table-column>
+
+    <el-table-column label="操作">
+      <template #default="scope">
+        <el-popconfirm
+          @confirm="() => deleteAdvertiserFunc(scope.row.advertiserId)"
+          title="确定删除该广告主吗?"
+        >
+          <template #reference>
+            <el-button link type="primary" size="small">删除</el-button>
+          </template>
+        </el-popconfirm>
+        <el-button link type="primary" size="small" @click="() => creatEditPlan(scope.row)">
+          修改
+        </el-button>
+
+        <el-popconfirm
+          v-if="scope.row.status == 6"
+          @confirm="
+            () =>
+              updateAdvertiserStatus({
+                id: scope.row.advertiserId,
+                status: 10,
+              }).then(() => onSubmit())
+          "
+          title="确定启用该广告主吗?"
+        >
+          <template #reference>
+            <el-button link type="primary" size="small"> 启用 </el-button>
+          </template>
+        </el-popconfirm>
+        <el-popconfirm
+          v-if="scope.row.status == 10"
+          @confirm="
+            () =>
+              updateAdvertiserStatus({
+                id: scope.row.advertiserId,
+                status: 6,
+              }).then(() => onSubmit())
+          "
+          title="确定禁用该广告主吗?"
+        >
+          <template #reference>
+            <el-button link type="primary" size="small"> 禁用 </el-button>
+          </template>
+        </el-popconfirm>
+      </template>
+    </el-table-column>
+  </el-table>
+  <br />
+  <el-pagination
+    background
+    layout="prev, pager, next"
+    :total="pages.total"
+    @current-change="page"
+  />
+
+  <el-dialog v-model="dialogVisible" title="广告主">
+    <el-form :model="form" label-width="auto" style="max-width: 600px">
+      <el-form-item label="广告主名称">
+        <el-input v-model="form.enterpriseName" />
+      </el-form-item>
+      <el-form-item label="联系人">
+        <el-input v-model="form.enterpriseContacts" />
+      </el-form-item>
+      <el-form-item label="手机号">
+        <el-input v-model="form.enterpriseTel" />
+      </el-form-item>
+      <el-form-item label="品牌">
+        <el-input v-model="form.enterpriseBrand" />
+      </el-form-item>
+      <el-form-item label="联系地址">
+        <el-input v-model="form.enterpriseAddress" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="createOrUpdate">确定</el-button>
+      </el-form-item>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import type { Pages } from '@/types/Tool'
+import type { Advertiser } from '@/types/Advertiser'
+import {
+  getAdvertiserList,
+  deleteAdvertiser,
+  updateAdvertiserStatus,
+  updateAdvertiser,
+  createAdvertiser,
+} from '@/api/index'
+import { ref } from 'vue'
+import { statusList } from '@/tool'
+import { ElMessage } from 'element-plus'
+const pages = ref<Pages>({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0,
+})
+const dialogVisible = ref(false)
+const form = ref<Advertiser>({
+  enterpriseName: '',
+  enterpriseTel: '',
+  enterpriseBrand: '',
+  enterpriseAddress: '',
+  enterpriseContacts: '',
+  advertiserId: undefined,
+})
+
+const tableData = ref<Advertiser[]>()
+
+const onSubmit = () => {
+  getAdvertiserList(pages.value).then(({ data }) => {
+    tableData.value = data.records
+  })
+}
+
+const page = (val: number) => {
+  pages.value.pageNum = val
+  onSubmit()
+}
+
+const creatEditPlan = (data?: Advertiser) => {
+  if (data != undefined) form.value = data
+  else
+    form.value = {
+      enterpriseName: '',
+      enterpriseTel: '',
+      enterpriseBrand: '',
+      enterpriseAddress: '',
+      enterpriseContacts: '',
+      advertiserId: undefined,
+    }
+  console.log(form.value)
+  dialogVisible.value = !dialogVisible.value
+}
+
+const deleteAdvertiserFunc = (id: number) => {
+  deleteAdvertiser(id).then(() => onSubmit())
+}
+
+const createOrUpdate = () => {
+  const Func = form.value.advertiserId == undefined ? createAdvertiser : updateAdvertiser
+  const p = {
+    enterpriseName: form.value.enterpriseName,
+    enterpriseTel: form.value.enterpriseTel,
+    enterpriseBrand: form.value.enterpriseBrand,
+    enterpriseAddress: form.value.enterpriseAddress,
+    enterpriseContacts: form.value.enterpriseContacts,
+    advertiserId: form.value.advertiserId,
+  }
+
+  Func(p).then(() => {
+    ElMessage.success(p.advertiserId == undefined ? '更新成功' : '修改成功')
+    onSubmit()
+    dialogVisible.value = false
+  })
+}
+
+onSubmit()
+</script>
+
+<style></style>

+ 195 - 0
src/views/AdvertisingSpaceDatePage.vue

@@ -0,0 +1,195 @@
+<!-- 推广位甘特图 -->
+<template>
+  <el-form :inline="true">
+    <el-form-item label="时间">
+      <el-date-picker
+        v-model="from.datas"
+        type="daterange"
+        format="YYYY-MM-DD"
+        value-format="YYYY-MM-DD"
+        range-separator="-"
+        start-placeholder="开始时间"
+        end-placeholder="结束时间"
+      />
+    </el-form-item>
+    <el-form-item label="站点">
+      <el-select
+        filterable
+        clearable
+        v-model="from.mediaId"
+        placeholder="站点"
+        style="width: 200px"
+      >
+        <el-option
+          v-for="item in SiteList"
+          :key="item.mediaId"
+          :label="item.mediaName"
+          :value="item.mediaId"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" @click="getDataList" :loading="loading">搜索</el-button>
+    </el-form-item>
+  </el-form>
+  <el-empty v-show="!dataList || !dataList.length" description="暂无数据" />
+  <div class="row" v-show="dataList && dataList.length">
+    <div class="col">广告位</div>
+    <div
+      class="col"
+      :style="'width: calc((100% - 100px)/' + dayCha + ')'"
+      v-for="v in tableHeader"
+      :key="v"
+      v-text="v"
+    ></div>
+  </div>
+  <div class="row" v-show="dataList && dataList.length" v-for="item in dataList" :key="item.name">
+    <div class="col" style="" v-text="item.name"></div>
+    <div
+      class="col"
+      :style="
+        'width: calc((100% - 100px)/' +
+        dayCha +
+        ');background-color: ' +
+        (v ? '#409eff' : '#ffffff00')
+      "
+      v-for="v in item.datesList"
+      :key="v"
+    >
+      <el-tooltip v-if="v" effect="dark" :content="v" placement="top-start">
+        <div style="width: 100%; height: 100%"></div>
+      </el-tooltip>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import type { AsSpaceItem, CreativesList, AsSpaceList } from '@/types/AsSpace'
+import type { Pages } from '@/types/Tool'
+import type { SiteList } from '@/types/Site'
+import { getAsSpaceList, getSiteList } from '@/api/index'
+import { formatDateSite } from '@/tool/index'
+
+const SiteList = ref<SiteList>([])
+getSiteList().then((res) => {
+  SiteList.value.push(...res.data)
+})
+
+const time = new Date().getTime()
+const loading = ref(false)
+const from = ref<Pages>({
+  pageNum: 1,
+  pageSize: 999999,
+  datas: [formatDateSite(time - 30 * 86400000, 'Y-M-D'), formatDateSite(time, 'Y-M-D')],
+})
+const dataList = ref<AsSpaceList[]>()
+const firstDay = ref<Date>()
+
+const changeDate = (date: string[][]): string[] => {
+  if (!firstDay.value) return []
+  const list: string[] = new Array(dayCha.value)
+  const e = dayCha.value || 0
+  for (let i = 0; i < e; i++) {
+    const li: string[] = []
+    const D = firstDay.value.getTime() + 86400000 * i
+    for (let p = 0; p < date.length; p++) {
+      const v = date[p]
+      const s = new Date(v[0] + ' 00:00:00').getTime()
+      const e = new Date(v[1] + ' 23:59:59').getTime()
+      if (D >= s && D <= e) {
+        li.push(v[2])
+      }
+    }
+    list[i] = li.join('、')
+  }
+  return list
+}
+
+const getDataList = () => {
+  const t = setTimeout(() => {
+    clearTimeout(t)
+    loading.value = true
+  }, 500)
+  if (!from.value.datas || !from.value.datas.length) return
+  getAsSpaceList({
+    pageNum: 1,
+    pageSize: 999999,
+    keyword: null,
+    status: 10,
+    mediaId: from.value.mediaId,
+    startDate: from.value.datas[0],
+    endDate: from.value.datas[1],
+  })
+    .then(({ data: { records } }) => {
+      if (!from.value.datas || !from.value.datas.length) return
+      firstDay.value = new Date(from.value.datas[0] + ' 00:00:00')
+      dayCha.value = DateDifference(from.value.datas[0], from.value.datas[1])
+      dataList.value = records.map((item: AsSpaceItem) => {
+        const dates = item.creativesList.map((v: CreativesList) => {
+          return [v.startDate, v.endDate, v.creativeName]
+        })
+        const datesList: string[] = changeDate(dates)
+        return {
+          name: item.slotName,
+          dates,
+          datesList,
+        }
+      })
+      renderTableHeader()
+      clearTimeout(t)
+      loading.value = false
+    })
+    .catch(() => {
+      clearTimeout(t)
+      loading.value = false
+    })
+}
+
+const DateDifference = (start: string, end: string) => {
+  console.log(start, end)
+  const startDate = new Date(start + ' 00:00:00')
+  const endDate = new Date(end + ' 00:00:00')
+  return Math.ceil((endDate.getTime() - startDate.getTime()) / 86400000) + 1
+}
+
+const dayCha = ref<number>()
+if (from.value.datas && from.value.datas.length)
+  dayCha.value = DateDifference(from.value.datas[0], from.value.datas[1])
+const tableHeader = ref<string[]>([])
+const renderTableHeader = () => {
+  if (!firstDay.value) return
+  const TH: string[] = []
+  const e = dayCha.value || 0
+  for (let i = 0; i < e; i++) {
+    const date = new Date(firstDay.value.getTime() + 86400000 * i)
+    TH.push(date.getDate().toString())
+  }
+  tableHeader.value = TH
+}
+
+getDataList()
+</script>
+
+<style scoped>
+.row {
+  /* 禁止换行 */
+  white-space: nowrap;
+  border: 1px solid var(--el-border-color);
+  border-right: none;
+  font-size: 0;
+}
+.row:not(:last-child) {
+  border-bottom: none;
+}
+.col {
+  display: inline-block;
+  height: 1.7em;
+  line-height: 1.7em;
+  overflow: hidden;
+  border-right: 1px solid var(--el-border-color);
+  font-size: 14px;
+  width: 100px;
+  text-align: center;
+}
+</style>

+ 266 - 0
src/views/AdvertisingSpacePage.vue

@@ -0,0 +1,266 @@
+<!-- 推广广告位列表 -->
+<template>
+  <el-form :inline="true">
+    <el-form-item label="广告位名称">
+      <el-input v-model="pages.keyword" placeholder="输入广告位关键词" clearable />
+    </el-form-item>
+    <!-- <el-form-item label="状态">
+      <el-select filterable clearable v-model="pages.status" style="width: 200px">
+        <el-option
+          v-for="item in statusList"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+        />
+      </el-select>
+    </el-form-item> -->
+    <el-form-item label="站点">
+      <el-select
+        filterable
+        clearable
+        v-model="pages.mediaId"
+        placeholder="站点"
+        style="width: 200px"
+      >
+        <el-option
+          v-for="item in SiteList"
+          :key="item.mediaId"
+          :label="item.mediaName"
+          :value="item.mediaId"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" @click="onSubmit">搜索</el-button>
+      <el-button type="primary" link @click="creatEditPlan">创建广告位</el-button>
+    </el-form-item>
+  </el-form>
+
+  <el-table :data="tableData" stripe style="width: 100%">
+    <el-table-column prop="slotName" label="名称" />
+    <el-table-column prop="createdTime" label="站点">
+      <template #default="scope">
+        {{ SiteDec(scope.row.mediaId) }}
+      </template>
+    </el-table-column>
+    <el-table-column prop="createdTime" label="栏目">
+      <template #default="scope">
+        {{ scope.row.catalog ? scope.row.catalog.catalogName : '' }}
+      </template>
+    </el-table-column>
+    <el-table-column prop="createdTime" label="最近更新">
+      <template #default="scope">
+        {{ formatDateSite(scope.row.updatedTime, 'Y-M-D') }}
+      </template>
+    </el-table-column>
+    <el-table-column label="操作">
+      <template #default="scope">
+        <el-popconfirm @confirm="deleteAsSpaceFun(scope.row.slotId)" title="确定删除该广告位吗?">
+          <template #reference>
+            <el-button link type="primary" size="small">删除</el-button>
+          </template>
+        </el-popconfirm>
+        <el-button link type="primary" size="small" @click="() => creatEditPlan(scope.row, true)">
+          修改
+        </el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+  <br />
+  <el-pagination
+    background
+    layout="prev, pager, next"
+    :total="pages.total"
+    @current-change="page"
+  />
+
+  <el-dialog v-model="dialogVisible" title="广告位">
+    <el-form ref="ruleFormRef" :rules="rules" :model="form" label-width="auto">
+      <el-form-item label="广告位名称" prop="slotName">
+        <el-input v-model="form.slotName" />
+      </el-form-item>
+      <el-form-item label="站点" prop="mediaId">
+        <el-select
+          filterable
+          v-model="form.mediaId"
+          :disabled="isUpdate"
+          placeholder="站点"
+          size="large"
+          @change="changeSite"
+        >
+          <el-option
+            v-for="item in SiteList"
+            :key="item.mediaId"
+            :label="item.mediaName"
+            :value="item.mediaId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="栏目" prop="catalogId">
+        <el-tree-select
+          :disabled="isUpdate"
+          v-model="form.catalogId"
+          :data="CatalogTreeData"
+          :render-after-expand="false"
+        />
+      </el-form-item>
+      <el-form-item label="宽" prop="width">
+        <el-input-number v-model="form.width" :min="0" /> px
+      </el-form-item>
+      <el-form-item label="高" prop="height">
+        <el-input-number v-model="form.height" :min="0" /> px
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="subCreateSpace(ruleFormRef)">确定</el-button>
+      </el-form-item>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import type { FormInstance, FormRules } from 'element-plus'
+import type { CatalogTree, CatalogTreeOri, IAsSpace, AsSpaceItem } from '@/types/AsSpace'
+import type { SiteList, SiteItem } from '@/types/Site'
+import type { Pages } from '@/types/Tool'
+import {
+  createAsSpace,
+  getSiteList,
+  getAsTree,
+  getAsSpaceList,
+  deleteAsSpace,
+  updateAsSpace,
+} from '@/api/index'
+import { formatDateSite } from '@/tool/index'
+import { ElMessage } from 'element-plus'
+import { ref, reactive } from 'vue'
+
+// import { statusList } from '@/tool'
+
+const dialogVisible = ref(false)
+const ruleFormRef = ref<FormInstance>()
+const form = ref<IAsSpace>({})
+const isUpdate = ref(false)
+
+const rules = reactive<
+  FormRules<{ slotName: string; mediaId: number; catalogId: number; width: number; height: number }>
+>({
+  slotName: [{ required: true, message: '请输入广告位名称', trigger: 'blur' }],
+  mediaId: [{ required: true, message: '请选择站点', trigger: 'change' }],
+  catalogId: [{ required: true, message: '请选择栏目', trigger: 'change' }],
+  width: [{ required: true, message: '请输入广告位宽度', trigger: 'blur' }],
+  height: [{ required: true, message: '请输入广告位高度', trigger: 'blur' }],
+})
+
+const pages = reactive<Pages>({
+  pageNum: 1,
+  pageSize: 10,
+  keyword: '',
+  status: 10,
+})
+
+const SiteList = ref<SiteList>([])
+const CatalogTreeData = ref<CatalogTree[]>([])
+
+const tableData = ref([])
+
+const changeSite = (val: number) => {
+  getAsTree(val).then(({ data }) => {
+    CatalogTreeData.value = toValueLabel(data)
+  })
+}
+
+const SiteDec = (mediaId: number): string | void => {
+  const item: SiteItem = SiteList.value.find((item) => item.mediaId === mediaId) || {}
+  return item.mediaName
+}
+
+const deleteAsSpaceFun = (id: number) => {
+  deleteAsSpace(id).then(() => {
+    ElMessage.success('删除成功')
+    onSubmit()
+  })
+}
+
+const toValueLabel = (data: CatalogTreeOri[]): CatalogTree[] => {
+  return data.map((item: CatalogTreeOri) => {
+    const p: CatalogTree = {
+      label: item.catalogName,
+      value: item.catalogId,
+    }
+    if (item.children !== undefined) p.children = toValueLabel(item.children)
+    return p
+  })
+}
+
+const onSubmit = () => {
+  getAsSpaceList({
+    pageNum: pages.pageNum,
+    pageSize: pages.pageSize,
+    keyword: pages.keyword ? pages.keyword : null,
+    status: pages.status,
+    mediaId: typeof pages.mediaId === 'number' ? pages.mediaId : null,
+  }).then(({ data }) => {
+    tableData.value = data.records || []
+    pages.total = data.total
+  })
+}
+
+const page = (val: number) => {
+  pages.pageNum = val
+  onSubmit()
+}
+
+const creatEditPlan = (data: AsSpaceItem, isUpdateF = false) => {
+  if (data != undefined) {
+    form.value = {
+      mediaId: data.mediaId,
+      catalogId: data.catalogId,
+      slotName: data.slotName,
+      width: data.width,
+      height: data.height,
+      adType: data.adType,
+      slotId: data.slotId,
+    }
+  }
+  dialogVisible.value = !dialogVisible.value
+  if (typeof data.mediaId === 'number') changeSite(data.mediaId)
+  isUpdate.value = isUpdateF
+}
+
+const subCreateSpace = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid) => {
+    if (!valid) return
+    if (!isUpdate.value)
+      createAsSpace(form.value).then(() => {
+        ElMessage({
+          message: '创建成功',
+          type: 'success',
+        })
+        dialogVisible.value = !dialogVisible.value
+        onSubmit()
+      })
+    else
+      updateAsSpace({
+        slotId: form.value.slotId,
+        slotName: form.value.slotName,
+        width: form.value.width,
+        height: form.value.height,
+      }).then(() => {
+        ElMessage({
+          message: '修改成功',
+          type: 'success',
+        })
+        dialogVisible.value = !dialogVisible.value
+        onSubmit()
+      })
+  })
+}
+
+onSubmit()
+getSiteList().then((res) => {
+  SiteList.value = res.data || []
+})
+</script>
+
+<style></style>

+ 142 - 0
src/views/LoginPage.vue

@@ -0,0 +1,142 @@
+<script setup lang="ts">
+// import { RouterLink, RouterView } from 'vue-router'
+import { ref } from 'vue'
+import { ElMessage } from 'element-plus'
+
+import type { UserData } from '@/types/User'
+import type { FormInstance } from 'element-plus'
+import { getCaptcha, login } from '@/api/index'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+let t: number | undefined = undefined
+const ruleFormRef = ref<FormInstance>()
+
+const form = ref<UserData>({
+  phone: '',
+  code: '',
+})
+const loading = ref(false)
+const countdown = ref(0)
+
+const rules = {
+  phone: [
+    {
+      required: true,
+      message: '请输入手机号',
+      trigger: 'blur',
+    },
+    {
+      pattern: /^1[3456789]\d{9}$/,
+      message: '请输入正确的手机号',
+      trigger: 'blur',
+    },
+  ],
+  code: [
+    {
+      required: true,
+      message: '请输入验证码',
+      trigger: 'blur',
+    },
+  ],
+}
+
+const code = () => {
+  if (!form.value.phone)
+    return ElMessage({
+      message: '请输入手机号',
+      type: 'warning',
+    })
+
+  countdown.value = 60
+  t = setInterval(() => {
+    if (--countdown.value <= 0) clearInterval(t)
+  }, 1000)
+  getCaptcha(form.value.phone).then(() => {
+    ElMessage({
+      message: '验证码获取成功',
+      type: 'success',
+    })
+  })
+}
+
+const onSubmit = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid) => {
+    if (valid) {
+      login(form.value).then(({ data }) => {
+        if (!data.token)
+          return ElMessage({
+            message: '登录失败',
+            type: 'error',
+          })
+        localStorage.setItem('token', data.token)
+        ElMessage({
+          message: '登录成功',
+          type: 'success',
+        })
+        localStorage.setItem('phone', form.value.phone || '')
+        router.replace('/authorized/originality')
+      })
+    }
+  })
+}
+</script>
+
+<template>
+  <div class="main">
+    <div class="bgTop"></div>
+    <div class="bgBottom">
+      <el-form
+        ref="ruleFormRef"
+        :rules="rules"
+        :model="form"
+        label-width="auto"
+        style="max-width: 600px"
+      >
+        <el-form-item label="手机号" prop="phone">
+          <el-input v-model="form.phone"> </el-input>
+        </el-form-item>
+        <el-form-item label="验证码" prop="code">
+          <el-input v-model="form.code">
+            <template #append>
+              <el-button type="primary" v-show="!countdown" :loading="loading" @click="code">
+                获取验证码
+              </el-button>
+              <span v-show="countdown">{{ countdown }}</span>
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="onSubmit(ruleFormRef)">登录</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.bgTop {
+  height: 60vh;
+  width: 120vw;
+  background-color: #4300ff;
+  position: relative;
+  top: -10vh;
+  left: -10vw;
+  transform: rotate(-5deg);
+}
+
+.bgBottom {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  margin: 0 auto;
+  width: 522px;
+  background-color: #ffffff;
+  box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
+  border-radius: 15px;
+  padding: 45px 93px;
+}
+</style>

+ 426 - 0
src/views/OriginalityPage.vue

@@ -0,0 +1,426 @@
+<!-- 创意列表页 -->
+<template>
+  <el-form :inline="true">
+    <el-form-item label="名称">
+      <el-input v-model="page.keyword" placeholder="输入创意关键词" clearable />
+    </el-form-item>
+    <el-form-item label="广告主">
+      <el-select
+        filterable
+        v-model="page.advertiserId"
+        placeholder="选择广告主"
+        clearable
+        style="width: 240px"
+      >
+        <el-option
+          v-for="item in AdvertiserList"
+          :key="item.advertiserId"
+          :label="item.enterpriseName"
+          :value="item.advertiserId"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" @click="getOriginality">搜索</el-button>
+      <el-button type="primary" link @click="() => creatEditPlan()">创建</el-button>
+    </el-form-item>
+  </el-form>
+
+  <el-table :data="tableData" stripe style="width: 100%">
+    <el-table-column prop="creativeName" label="名称" />
+    <el-table-column prop="statusDesc" label="状态" />
+    <el-table-column prop="startDate" label="生效日期" />
+    <el-table-column prop="endDate" label="结束日期" />
+    <el-table-column label="操作">
+      <template #default="scope">
+        <el-popconfirm
+          title="确定删除该创意吗?"
+          @confirm="deleteOriginalityFun(scope.row.creativeId)"
+        >
+          <template #reference>
+            <el-button link type="primary" size="small">删除</el-button>
+          </template>
+        </el-popconfirm>
+        <el-popconfirm
+          title="确定启用该创意吗?"
+          v-if="scope.row.status == 6 || scope.row.status == 8 || scope.row.status == 4"
+          @confirm="changeStatus(scope.row.creativeId, 10, '启用成功')"
+        >
+          <template #reference>
+            <el-button link type="primary" size="small">启用</el-button>
+          </template>
+        </el-popconfirm>
+        <el-popconfirm
+          title="确定禁用该创意吗?"
+          v-if="scope.row.status == 10"
+          @confirm="changeStatus(scope.row.creativeId, 6, '禁用成功')"
+        >
+          <template #reference>
+            <el-button link type="primary" size="small">禁用</el-button>
+          </template>
+        </el-popconfirm>
+        <el-button link type="primary" size="small" @click="() => creatEditPlan(scope.row)">
+          修改
+        </el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+  <br />
+  <el-pagination
+    background
+    layout="prev, pager, next"
+    :total="page.total"
+    @current-change="changePage"
+  />
+
+  <el-dialog v-model="dialogVisible" title="创意" width="700">
+    <el-form :model="form" label-width="auto">
+      <el-form-item label="创意名称">
+        <el-input v-model="form.creativeName" prop="creativeName" />
+      </el-form-item>
+      <el-form-item label="广告主">
+        <el-select filterable clearable v-model="form.advertiserId" placeholder="选择广告主">
+          <el-option
+            v-for="item in AdvertiserList"
+            :key="item.advertiserId"
+            :label="item.enterpriseName"
+            :value="item.advertiserId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="周期" prop="dates">
+        <el-date-picker
+          v-model="form.dates"
+          type="daterange"
+          @change="() => getAs()"
+          format="YYYY-MM-DD"
+          value-format="YYYY-MM-DD"
+          range-separator="到"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+        />
+      </el-form-item>
+      <el-form-item label="站点" prop="mediaIds">
+        <el-checkbox-group @change="() => getAs()" v-model="form.mediaIds">
+          <el-checkbox
+            :value="v.mediaId"
+            disabled
+            size="large"
+            v-for="v in siteList"
+            :key="v.mediaId"
+            >{{ v.mediaName }}</el-checkbox
+          >
+        </el-checkbox-group>
+      </el-form-item>
+      <el-form-item label="创意排期" prop="intervalType">
+        <div>
+          <el-radio-group
+            v-model="form.intervalType"
+            @change="(val: number) => changeIntervalType(val)"
+          >
+            <el-radio :value="1" size="large">全时段</el-radio>
+            <el-radio :value="2" size="large">自定义</el-radio>
+          </el-radio-group>
+          <el-table v-if="form.intervalType === 2" :data="tableSchedulingData" style="width: 500px">
+            <el-table-column fixed="left" prop="weekName" label="" width="60" />
+            <el-table-column fixed="left" prop="selectAll" label="全选" width="60">
+              <template #default="scope">
+                <el-checkbox
+                  @change="(val: boolean) => selectAll(val, scope.row, scope.$index)"
+                  v-model="scope.row.selectAll"
+                  size="large"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column :label="i - 1 + ''" v-for="i in 24" :key="i" width="50">
+              <template #default="scope">
+                <el-checkbox
+                  @change="(val: boolean) => select(val, scope.row, scope.$index)"
+                  v-model="scope.row[i - 1]"
+                  size="large"
+                />
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </el-form-item>
+      <el-form-item label="投放位置" prop="slot">
+        <el-select
+          filterable
+          multiple
+          collapse-tags
+          v-model="form.slot"
+          placeholder="选择位置"
+          clearable
+        >
+          <el-option
+            v-for="item in options"
+            :key="item.slotId"
+            :label="item.slotName"
+            :value="item.slotId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="投放形式" prop="showType">
+        <el-select filterable v-model="form.showType">
+          <el-option
+            v-for="item in typeOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="createOrUpdate">确定</el-button>
+        <el-button v-if="form.creativeId" type="primary" @click="linkSourceMaterial">
+          关联素材
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import type {
+  OriginalityItem,
+  OriginalityList,
+  TimeInterval,
+  TimeIntervalList,
+  AdvertiserItem,
+} from '@/types/AdListPage'
+import type { AsSpaceItem } from '@/types/AsSpace'
+import type { Pages } from '@/types/Tool'
+import type { SiteList } from '@/types/Site'
+import {
+  getOriginalityList,
+  getSiteList,
+  getOriginalityDetail,
+  getAsList,
+  deleteOriginality,
+  getAdvertiserList,
+  createOriginality,
+  updateOriginality,
+  updateOriginalityStatus,
+} from '@/api/index'
+import { ElLoading, ElMessage } from 'element-plus'
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { formatDate } from '@/tool/index'
+const router = useRouter()
+const dialogVisible = ref(false)
+const form = ref<OriginalityItem>({})
+const page = ref<Pages>({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0,
+  keyword: '',
+})
+const AdvertiserList = ref<AdvertiserItem[]>()
+
+const options = ref<AsSpaceItem[]>([])
+
+const tableSchedulingData = ref<TimeIntervalList>([])
+
+const typeOptions = [
+  {
+    value: 1,
+    label: '轮播',
+  },
+  {
+    value: 2,
+    label: '交替',
+  },
+]
+
+const tableData = ref<OriginalityList>([])
+const siteList = ref<SiteList>([])
+
+// 获取站点信息
+getSiteList().then((res) => {
+  siteList.value = res.data
+})
+// 广告主列表
+getAdvertiserList({
+  pageNum: 1,
+  pageSize: 999999,
+}).then(({ data }) => {
+  AdvertiserList.value = data && data.records && data.records.length ? data.records : []
+})
+
+// 获取广告位
+const getAs = () => {
+  const data = form.value
+  let timeInterval = ''
+  for (let i = 0; i < tableSchedulingData.value.length; i++) {
+    const v = tableSchedulingData.value[i]
+    for (let i = 0; i < 24; i++) {
+      timeInterval += v[i] ? '1' : '0'
+    }
+  }
+  getAsList({
+    mediaIds: data.mediaIds ? data.mediaIds : [],
+    startDate: data.dates ? data.dates[0] : '',
+    endDate: data.dates ? data.dates[1] : '',
+    intervalType: data.intervalType ? data.intervalType : undefined,
+    timeInterval,
+  }).then(({ data }) => {
+    options.value = data
+  })
+}
+
+const changeIntervalType = (val: number) => {
+  if (val === 2) return
+  tableSchedulingData.value = new Array(7).fill(new Array(24).fill(true))
+  getAs()
+}
+
+const deleteOriginalityFun = (id: number) => {
+  deleteOriginality(id).then(() => {
+    getOriginality()
+  })
+}
+
+const changePage = (val: number) => {
+  page.value.pageNum = val
+  getOriginality()
+}
+
+const selectAll = (val: boolean, item: TimeInterval, index: number) => {
+  const v: TimeInterval = tableSchedulingData.value[index]
+  for (let i = 0; i < 24; i++) {
+    v[i] = val
+  }
+  getAs()
+}
+
+const select = (val: boolean, item: TimeInterval, index: number) => {
+  const v: TimeInterval = tableSchedulingData.value[index]
+  let all = true
+  for (let i = 0; i < 24; i++) {
+    const iV = v[i] ? true : false
+    all = all && iV
+    if (!all) break
+  }
+  if (all !== v.selectAll) v.selectAll = all
+  getAs()
+}
+
+const creatEditPlan = async (data?: OriginalityItem) => {
+  const date_ = formatDate(new Date().getTime())
+  let D: OriginalityItem = {
+    intervalType: 1,
+    dates: [
+      date_.fmtYear + '-' + date_.fmtMonth + '-' + date_.fmtDay,
+      date_.fmtYear + '-' + date_.fmtMonth + '-' + date_.fmtDay,
+    ],
+    timeInterval:
+      '111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111',
+  }
+  if (data != undefined) {
+    const loading = ElLoading.service({
+      lock: true,
+      text: '加载中',
+      background: 'rgba(0, 0, 0, 0.7)',
+    })
+    try {
+      const res = await getOriginalityDetail(Number(data.creativeId))
+      D = res.data || {}
+      loading.close()
+      const slot = D.slotsList ? D.slotsList.map((v) => v.slotId) : D.slotIds ? D.slotIds : []
+      form.value = {
+        ...D,
+        slot,
+        dates: [D.startDate ? D.startDate : '', D.endDate ? D.endDate : ''],
+        mediaIds:
+          D.mediaList && typeof D.mediaList == 'object'
+            ? D.mediaList.map((v) => (typeof v !== 'number' ? v.mediaId : -1))
+            : [],
+      }
+    } catch (err) {
+      console.error(err)
+      loading.close()
+      form.value = {
+        ...D,
+      }
+    }
+  } else {
+    form.value = {
+      ...D,
+    }
+    if (typeof siteList.value[0].mediaId === 'number')
+      form.value.mediaIds = [siteList.value[0].mediaId]
+  }
+
+  const weeks: string[] = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
+  const scheduling: TimeIntervalList = []
+  for (let i = 0; i < 7; i++) {
+    const times = (D.timeInterval || '').slice(i * 24, i * 24 + 24).split('')
+    const p: TimeInterval = {
+      selectAll: true,
+      weekName: weeks[i],
+    }
+    for (let i = 0; i < 24; i++) {
+      p[i] = times[i] === '1' ? true : false
+      if (!p[i]) p.selectAll = false
+    }
+    scheduling.push(p)
+  }
+  tableSchedulingData.value = scheduling
+  dialogVisible.value = !dialogVisible.value
+
+  getAs()
+}
+
+const getOriginality = () => {
+  getOriginalityList(page.value).then((res) => {
+    const data = res.data || {}
+    page.value.total = data.total
+    tableData.value = data.records
+  })
+}
+
+// 创建或更新创意
+const createOrUpdate = () => {
+  const Func = form.value.creativeId ? updateOriginality : createOriginality
+  let timeInterval = ''
+  for (let i = 0; i < tableSchedulingData.value.length; i++) {
+    const v = tableSchedulingData.value[i]
+    for (let i = 0; i < 24; i++) {
+      timeInterval += v[i] ? '1' : '0'
+    }
+  }
+  const p: OriginalityItem = {
+    creativeId: form.value.creativeId,
+    advertiserId: form.value.advertiserId,
+    creativeName: form.value.creativeName,
+    intervalType: form.value.intervalType,
+    timeInterval,
+    startDate: form.value.dates ? form.value.dates[0] : '',
+    endDate: form.value.dates ? form.value.dates[1] : '',
+    showType: form.value.showType,
+    mediaIds: form.value.mediaIds,
+    slotIds: form.value.slot,
+  }
+  Func(p).then(({ data }) => {
+    ElMessage.success(form.value.creativeId ? '更新成功' : '创建成功')
+    form.value.creativeId = data.creativeId
+  })
+}
+
+const changeStatus = (id: number, status: number, text: string) => {
+  updateOriginalityStatus({ id, status }).then(() => {
+    ElMessage.success(text)
+    getOriginality()
+  })
+}
+
+const linkSourceMaterial = () => {
+  // 跳转到相关素材
+  router.push('/authorized/sourceMaterial?creativeId=' + form.value.creativeId)
+}
+
+getOriginality()
+</script>
+
+<style></style>

+ 226 - 0
src/views/ReportFormsPage.vue

@@ -0,0 +1,226 @@
+<!-- 广告报表列表页 -->
+<template>
+  <div v-loading="loading">
+    <el-form :inline="true">
+      <el-form-item label="周期">
+        <el-date-picker
+          v-model="pages.datas"
+          type="daterange"
+          format="YYYY-MM-DD"
+          value-format="YYYY-MM-DD"
+          range-separator="到"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+        />
+      </el-form-item>
+      <el-form-item label="类型">
+        <el-select clearable v-model="pages.type" style="width: 200px">
+          <el-option label="日" value="day" />
+          <el-option label="月" value="month" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="区域">
+        <el-select filterable clearable v-model="pages.province" style="width: 200px">
+          <el-option
+            v-for="item in provinces"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="广告主">
+        <el-select
+          multiple
+          collapse-tags
+          filterable
+          clearable
+          v-model="pages.advertiserIds"
+          style="width: 200px"
+        >
+          <el-option
+            v-for="item in advertisers"
+            :key="item.advertiserId"
+            :label="item.enterpriseName"
+            :value="item.advertiserId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创意">
+        <el-select
+          multiple
+          collapse-tags
+          filterable
+          clearable
+          v-model="pages.creativeIds"
+          style="width: 200px"
+        >
+          <el-option
+            v-for="item in originalitys"
+            :key="item.creativeId"
+            :label="item.creativeName"
+            :value="item.creativeId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="素材">
+        <el-select
+          multiple
+          collapse-tags
+          filterable
+          clearable
+          v-model="pages.stuffIds"
+          style="width: 200px"
+        >
+          <el-option
+            v-for="item in SourceMaterials"
+            :key="item.stuffId"
+            :label="item.stuffName"
+            :value="item.stuffId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSubmit">搜索</el-button>
+        <el-button type="primary" @click="() => onSubmit(true)">导出</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table :data="tableData" stripe style="width: 100%">
+      <el-table-column prop="sdate" label="日期" />
+      <el-table-column prop="requestCount" label="请求数" />
+      <el-table-column prop="exposureCount" label="曝光数" />
+      <el-table-column prop="clickCount" label="点击数" />
+      <el-table-column prop="uvCount" label="用户数" />
+    </el-table>
+    <br />
+    <el-pagination
+      background
+      layout="prev, pager, next"
+      :total="pages.total"
+      @current-change="page"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { OriginalityItem } from '@/types/AdListPage'
+import type { Advertiser } from '@/types/Advertiser'
+import type { Pages } from '@/types/Tool'
+import type { SourceMaterialList } from '@/types/SourceMaterial'
+import type { IReportItem } from '@/types/Report'
+import {
+  getAdvertiserList,
+  getOriginalityList,
+  getMaterialList,
+  getAdvertiserReport,
+  getAdvertiserMonthReport,
+  getProvinceCity,
+} from '@/api/index'
+import { formatDate } from '@/tool/index'
+import { reactive, ref } from 'vue'
+const date_ = formatDate(new Date().getTime())
+const pages = reactive<Pages>({
+  pageNum: 1,
+  pageSize: 10,
+  advertiserIds: [],
+  creativeIds: [],
+  stuffIds: [],
+  province: '',
+  datas: [
+    date_.fmtYear + '-' + date_.fmtMonth + '-' + date_.fmtDay,
+    date_.fmtYear + '-' + date_.fmtMonth + '-' + date_.fmtDay,
+  ],
+  total: 0,
+  type: 'day',
+})
+const advertisers = ref<Advertiser[]>([])
+const originalitys = ref<OriginalityItem[]>([])
+const SourceMaterials = ref<SourceMaterialList>([])
+const provinces = ref<{ label: string; value: string }[]>([])
+const tableData = ref<IReportItem[]>([])
+const loading = ref(false)
+
+const getList = () => {
+  getAdvertiserList({
+    pageNum: 1,
+    pageSize: 999999,
+  }).then(({ data }) => {
+    // 广告主列表
+    advertisers.value = data.records || []
+  })
+  getOriginalityList({
+    pageNum: 1,
+    pageSize: 999999,
+    keyword: '',
+  }).then(({ data }) => {
+    // 创意列表
+    originalitys.value = data.records || []
+  })
+  getMaterialList({
+    pageNum: 1,
+    pageSize: 999999,
+  }).then(({ data }) => {
+    // 素材列表
+    SourceMaterials.value = data.records || []
+  })
+  getProvinceCity().then(({ data }) => {
+    provinces.value = data.area.map((item: { name: string }) => {
+      return {
+        label: item.name,
+        value: item.name,
+      }
+    })
+  })
+}
+
+const onSubmit = (isExport = false) => {
+  const p = {
+    advertiserIds: pages.advertiserIds,
+    creativeIds: pages.creativeIds,
+    stuffIds: pages.stuffIds,
+    startDate: pages.datas ? pages.datas[0] : '',
+    endDate: pages.datas ? pages.datas[1] : '',
+    provinces: [pages.province || ''],
+    pageNum: pages.pageNum,
+    pageSize: isExport ? 999999 : pages.pageSize,
+  }
+  const Func = pages.type == 'day' ? getAdvertiserReport : getAdvertiserMonthReport
+  loading.value = true
+  Func(p).then(({ data }) => {
+    if (!isExport) {
+      loading.value = false
+      tableData.value = data.records
+      pages.total = data.total
+      return
+    }
+    if (!data.records || data.records.length == 0) {
+      loading.value = false
+      return
+    }
+    // 生成数据
+    let strcsv = 'data:text/csv;charset=utf-8,\uFEFF日期,请求数,曝光数,点击数,用户数\r\n'
+    ;(data.records || []).map((v: IReportItem) => {
+      strcsv += `${v.sdate},${v.requestCount},${v.exposureCount},${v.clickCount},${v.uvCount},\r\n`
+    })
+    // 导出
+    let link: HTMLAnchorElement | undefined = document.createElement('a')
+    link.id = 'download-csv'
+    link.setAttribute('href', encodeURI(strcsv))
+    link.setAttribute('download', '广告' + '.csv')
+    link.click()
+    link = undefined
+    loading.value = false
+  })
+}
+
+const page = (val: number) => {
+  pages.pageNum = val
+  onSubmit()
+}
+
+getList()
+onSubmit()
+</script>
+
+<style></style>

+ 225 - 0
src/views/ReportSpaceFormsPage.vue

@@ -0,0 +1,225 @@
+<!-- 广告报表列表页 -->
+<template>
+  <div v-loading="loading">
+    <el-form :inline="true">
+      <el-form-item label="周期">
+        <el-date-picker
+          v-model="pages.datas"
+          type="daterange"
+          format="YYYY-MM-DD"
+          value-format="YYYY-MM-DD"
+          range-separator="到"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+        />
+      </el-form-item>
+      <el-form-item label="类型">
+        <el-select clearable v-model="pages.type" style="width: 200px">
+          <el-option label="日" value="day" />
+          <el-option label="月" value="month" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="区域">
+        <el-select filterable clearable v-model="pages.province" style="width: 200px">
+          <el-option
+            v-for="item in provinces"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="广告主">
+        <el-select
+          multiple
+          collapse-tags
+          filterable
+          clearable
+          v-model="pages.advertiserIds"
+          style="width: 200px"
+        >
+          <el-option
+            v-for="item in advertisers"
+            :key="item.advertiserId"
+            :label="item.enterpriseName"
+            :value="item.advertiserId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创意">
+        <el-select
+          multiple
+          collapse-tags
+          filterable
+          clearable
+          v-model="pages.creativeIds"
+          style="width: 200px"
+        >
+          <el-option
+            v-for="item in originalitys"
+            :key="item.creativeId"
+            :label="item.creativeName"
+            :value="item.creativeId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="素材">
+        <el-select
+          multiple
+          collapse-tags
+          filterable
+          clearable
+          v-model="pages.stuffIds"
+          style="width: 200px"
+        >
+          <el-option
+            v-for="item in SourceMaterials"
+            :key="item.stuffId"
+            :label="item.stuffName"
+            :value="item.stuffId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSubmit">搜索</el-button>
+        <el-button type="primary" @click="() => onSubmit()">导出</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table :data="tableData" stripe style="width: 100%">
+      <el-table-column prop="sdate" label="日期" />
+      <el-table-column prop="requestCount" label="请求数" />
+      <el-table-column prop="exposureCount" label="曝光数" />
+      <el-table-column prop="clickCount" label="点击数" />
+      <el-table-column prop="uvCount" label="用户数" />
+    </el-table>
+    <br />
+    <el-pagination
+      background
+      layout="prev, pager, next"
+      :total="pages.total"
+      @current-change="page"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { OriginalityItem } from '@/types/AdListPage'
+import type { Advertiser } from '@/types/Advertiser'
+import type { Pages } from '@/types/Tool'
+import type { SourceMaterialList } from '@/types/SourceMaterial'
+import type { IReportItem } from '@/types/Report'
+import {
+  getAdvertiserList,
+  getOriginalityList,
+  getMaterialList,
+  getMediaReport,
+  getMediaMonthReport,
+  getProvinceCity,
+} from '@/api/index'
+import { formatDate } from '@/tool/index'
+import { reactive, ref } from 'vue'
+const tableData = ref<IReportItem[]>([])
+const date_ = formatDate(new Date().getTime())
+const pages = reactive<Pages>({
+  pageNum: 1,
+  pageSize: 10,
+  advertiserIds: [],
+  creativeIds: [],
+  stuffIds: [],
+  province: '',
+  datas: [
+    date_.fmtYear + '-' + date_.fmtMonth + '-' + date_.fmtDay,
+    date_.fmtYear + '-' + date_.fmtMonth + '-' + date_.fmtDay,
+  ],
+  total: 0,
+  type: 'day',
+})
+const advertisers = ref<Advertiser[]>([])
+const originalitys = ref<OriginalityItem[]>([])
+const SourceMaterials = ref<SourceMaterialList>([])
+const loading = ref(false)
+const provinces = ref<{ label: string; value: string }[]>([])
+
+const getList = () => {
+  getAdvertiserList({
+    pageNum: 1,
+    pageSize: 999999,
+  }).then(({ data }) => {
+    // 广告主列表
+    advertisers.value = data.records || []
+  })
+  getOriginalityList({
+    pageNum: 1,
+    pageSize: 999999,
+    keyword: '',
+  }).then(({ data }) => {
+    // 创意列表
+    originalitys.value = data.records || []
+  })
+  getMaterialList({
+    pageNum: 1,
+    pageSize: 999999,
+  }).then(({ data }) => {
+    // 素材列表
+    SourceMaterials.value = data.records || []
+  })
+  getProvinceCity().then(({ data }) => {
+    provinces.value = data.area.map((item: { name: string }) => {
+      return {
+        label: item.name,
+        value: item.name,
+      }
+    })
+  })
+}
+
+const onSubmit = (isExport = false) => {
+  const p = {
+    advertiserIds: pages.advertiserIds,
+    creativeIds: pages.creativeIds,
+    stuffIds: pages.stuffIds,
+    startDate: pages.datas ? pages.datas[0] : '',
+    endDate: pages.datas ? pages.datas[1] : '',
+    provinces: [pages.province || ''],
+    pageNum: pages.pageNum,
+    pageSize: isExport ? 999999 : pages.pageSize,
+  }
+  const Func = pages.type == 'day' ? getMediaReport : getMediaMonthReport
+  Func(p).then(({ data }) => {
+    if (!isExport) {
+      loading.value = false
+      tableData.value = data.records
+      pages.total = data.total
+      return
+    }
+    if (!data.records || data.records.length == 0) {
+      loading.value = false
+      return
+    }
+    // 生成数据
+    let strcsv = 'data:text/csv;charset=utf-8,\uFEFF日期,请求数,曝光数,点击数,用户数\r\n'
+    ;(data.records || []).map((v: IReportItem) => {
+      strcsv += `${v.sdate},${v.requestCount},${v.exposureCount},${v.clickCount},${v.uvCount},\r\n`
+    })
+    // 导出
+    let link: HTMLAnchorElement | undefined = document.createElement('a')
+    link.id = 'download-csv'
+    link.setAttribute('href', encodeURI(strcsv))
+    link.setAttribute('download', '广告位' + '.csv')
+    link.click()
+    link = undefined
+    loading.value = false
+  })
+}
+
+const page = (val: number) => {
+  pages.pageNum = val
+  onSubmit()
+}
+
+getList()
+onSubmit()
+</script>
+
+<style></style>

+ 191 - 0
src/views/SitePage.vue

@@ -0,0 +1,191 @@
+<!-- 站点列表页 -->
+<template>
+  <el-table :data="tableData" stripe style="width: 100%">
+    <el-table-column prop="mediaName" label="名称" />
+    <el-table-column prop="url" label="地址" />
+    <el-table-column prop="createdTime" label="创建日期">
+      <template #default="scope">
+        {{ formatDateSite(scope.row.createdTime, 'Y-M-D') }}
+      </template>
+    </el-table-column>
+    <el-table-column prop="updatedTime" label="更新日期">
+      <template #default="scope">
+        {{ formatDateSite(scope.row.updatedTime, 'Y-M-D') }}
+      </template>
+    </el-table-column>
+    <el-table-column label="操作">
+      <template #default="scope">
+        <el-button
+          link
+          type="primary"
+          @click="show(scope.row.mediaId, scope.row.mediaName)"
+          size="small"
+          :loading="loading"
+        >
+          查看栏目
+        </el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+
+  <el-dialog v-model="dialogVisible" :title="form.site">
+    <el-table :data="catalogData" row-key="catalogId" border default-expand-all>
+      <el-table-column prop="catalogName" label="名称">
+        <template #default="scope">
+          <el-input v-model="scope.row.catalogName" style="width: 240px" v-if="scope.row.isEdit" />
+          <span v-else v-text="scope.row.catalogName"></span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作">
+        <template #default="scope">
+          <el-button
+            v-if="scope.row.isEdit"
+            link
+            type="primary"
+            size="small"
+            @click="() => saveEdit(scope.row)"
+            :loading="loading"
+          >
+            确定
+          </el-button>
+          <div v-else>
+            <el-button
+              link
+              type="primary"
+              size="small"
+              @click="() => Edit(scope.row, catalogData, true)"
+            >
+              修改
+            </el-button>
+            <el-button link type="primary" size="small" @click="() => Add(scope.row, catalogData)">
+              添加
+            </el-button>
+            <el-popconfirm @confirm="deleteSiteFun(scope.row.catalogId)" title="确定删除该栏目吗?">
+              <template #reference>
+                <el-button link type="primary" size="small">删除</el-button>
+              </template>
+            </el-popconfirm>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-button
+      link
+      type="primary"
+      size="small"
+      @click="
+        () => (catalogData = [...catalogData, { isEdit: true, parentId: 0, mediaId: mediaId[0] }])
+      "
+    >
+      添加
+    </el-button>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import type { SiteList } from '@/types/Site'
+import type { CatalogTreeOri } from '@/types/Catalog'
+import { reactive, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getSiteList, deleteAsTree, getAsTree, updateAsTree, createAsTree } from '@/api/index'
+
+import { formatDateSite } from '@/tool/index'
+
+const loading = ref(false)
+const dialogVisible = ref(false)
+const form = reactive({
+  site: '',
+})
+
+const tableData = ref<SiteList>([])
+const catalogData = ref<CatalogTreeOri[]>([])
+
+let mediaId: [number, string] = [-1, '']
+
+getSiteList().then((res) => {
+  tableData.value.push(...res.data)
+})
+
+const deleteSiteFun = (id: number) => {
+  loading.value = true
+  deleteAsTree(id)
+    .then(() => {
+      loading.value = false
+      show(mediaId[0], mediaId[1])
+      ElMessage({
+        message: '删除成功',
+        type: 'success',
+      })
+    })
+    .catch(() => {
+      loading.value = false
+    })
+}
+
+const show = (id: number, mediaName: string) => {
+  loading.value = true
+  mediaId = [id, mediaName]
+  getAsTree(id)
+    .then(({ data }) => {
+      catalogData.value = data
+      form.site = mediaName
+      dialogVisible.value = true
+      loading.value = false
+    })
+    .catch(() => {
+      loading.value = false
+    })
+}
+
+const Edit = (row: CatalogTreeOri, list: CatalogTreeOri[], value = false) => {
+  for (let i = 0; i < list.length; i++) {
+    const v = list[i]
+    if (v.catalogId === row.catalogId) {
+      v.isEdit = value
+      break
+    } else if (v.children && v.children.length) {
+      Edit(row, v.children, value)
+    }
+  }
+}
+
+const saveEdit = (row: CatalogTreeOri) => {
+  loading.value = true
+  const exce = row.catalogId ? updateAsTree : createAsTree
+  const p = row.catalogId
+    ? {
+        catalogId: row.catalogId,
+        catalogName: row.catalogName,
+      }
+    : {
+        mediaId: row.mediaId,
+        parentId: row.parentId,
+        catalogName: row.catalogName,
+      }
+
+  exce(p).then(() => {
+    loading.value = false
+    show(mediaId[0], mediaId[1])
+    ElMessage({
+      message: row.catalogId ? '修改成功' : '创建成功',
+      type: 'success',
+    })
+  })
+}
+
+const Add = (row: CatalogTreeOri, list: CatalogTreeOri[]) => {
+  const sonItem: CatalogTreeOri = { isEdit: true, parentId: row.catalogId, mediaId: row.mediaId }
+  for (let i = 0; i < list.length; i++) {
+    const v = list[i]
+    if (v.catalogId === row.catalogId) {
+      v.children = [...(v.children || []), sonItem]
+      break
+    } else if (v.children && v.children.length) {
+      Add(row, v.children)
+    }
+  }
+}
+</script>
+
+<style></style>

+ 85 - 0
src/views/SkeletonPage.vue

@@ -0,0 +1,85 @@
+<script setup lang="ts">
+// RouterLink
+import { ref } from 'vue'
+import { RouterView } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
+
+const phone: string = localStorage.getItem('phone') || ''
+
+const router = useRouter()
+const route = useRoute()
+
+const deact = ref(route.path)
+
+const handleSelect = (key: string) => {
+  // 如果不是/开头的字符串则return
+  if (key.indexOf('/') !== 0) return
+  router.push(key)
+}
+
+const loginout = () => {
+  localStorage.removeItem('phone')
+  localStorage.removeItem('token')
+  router.replace({ path: '/login' })
+}
+</script>
+
+<template>
+  <el-container class="main">
+    <el-aside>
+      <!-- 左侧侧边栏容器 -->
+      <div class="logo">
+        <span v-text="phone" style="vertical-align: middle"></span>
+        <el-button type="warning" @click="loginout" link> 退出 </el-button>
+      </div>
+      <el-menu :default-active="deact" @select="handleSelect">
+        <el-sub-menu index="no-1">
+          <template #title>
+            <span>广告</span>
+          </template>
+          <!-- <el-menu-item index="/authorized/adlist">广告计划</el-menu-item> -->
+          <el-menu-item index="/authorized/originality">广告创意</el-menu-item>
+          <el-menu-item index="/authorized/sourceMaterial">广告素材</el-menu-item>
+        </el-sub-menu>
+        <el-menu-item index="/authorized/site">
+          <span>站点</span>
+        </el-menu-item>
+        <el-menu-item index="/authorized/advertiser">
+          <span>广告主</span>
+        </el-menu-item>
+        <el-sub-menu index="3">
+          <template #title>
+            <span>广告位</span>
+          </template>
+          <el-menu-item index="/authorized/advertisingSpace">广告位</el-menu-item>
+          <el-menu-item index="/authorized/advertisingSpaceDate">广告位占用情况</el-menu-item>
+        </el-sub-menu>
+        <el-sub-menu index="4">
+          <template #title>
+            <span>报表</span>
+          </template>
+          <el-menu-item index="/authorized/reportForms">广告报表</el-menu-item>
+          <el-menu-item index="/authorized/reportSpaceForms">广告位报表</el-menu-item>
+        </el-sub-menu>
+      </el-menu>
+    </el-aside>
+    <el-main>
+      <!-- 右侧内容容器 -->
+      <RouterView />
+    </el-main>
+  </el-container>
+</template>
+
+<style scoped>
+.logo {
+  height: 60px;
+  line-height: 60px;
+  background: #409eff;
+  color: #fff;
+  text-align: center;
+}
+
+.el-menu--horizontal {
+  --el-menu-horizontal-height: calc(100vh + 60px);
+}
+</style>

+ 285 - 0
src/views/SourceMaterialPage.vue

@@ -0,0 +1,285 @@
+<!-- 素材列表页 -->
+<template>
+  <el-form :inline="true">
+    <el-form-item label="素材名称">
+      <el-input v-model="pages.keyword" placeholder="输入素材关键词" clearable />
+    </el-form-item>
+    <el-form-item label="选择创意">
+      <el-select
+        filterable
+        v-model="pages.creativeId"
+        placeholder="选择创意"
+        clearable
+        style="width: 240px"
+      >
+        <el-option
+          v-for="item in originalityList"
+          :key="item.creativeId"
+          :label="item.creativeName"
+          :value="item.creativeId"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" @click="onSubmit">搜索</el-button>
+      <el-button type="primary" link @click="() => creatEditPlan()">创建素材</el-button>
+    </el-form-item>
+  </el-form>
+
+  <el-table :data="tableData" stripe style="width: 100%">
+    <el-table-column prop="stuffName" label="名称" />
+    <el-table-column prop="stuffName" label="状态">
+      <template #default="scope">
+        {{ getStatus(scope.row.status) }}
+      </template>
+    </el-table-column>
+    <el-table-column prop="stuffName" label="创意">
+      <template #default="scope">
+        {{ OriginalityName(scope.row.creativeId) }}
+      </template>
+    </el-table-column>
+    <el-table-column prop="width" label="宽(px)" width="80" />
+    <el-table-column prop="height" label="高(px)" width="80" />
+    <el-table-column prop="landingPage" label="跳转地址" />
+    <el-table-column prop="updatedTime" label="最近更新">
+      <template #default="scope">
+        {{ formatDateSite(scope.row.updatedTime, 'Y-M-D') }}
+      </template>
+    </el-table-column>
+    <el-table-column label="操作">
+      <template #default="scope">
+        <el-popconfirm @confirm="deleteMaterialFunc(scope.row.stuffId)" title="确定删除该广告吗?">
+          <template #reference>
+            <el-button link type="primary" size="small">删除</el-button>
+          </template>
+        </el-popconfirm>
+        <el-popconfirm
+          title="确定启用该素材吗?"
+          v-if="scope.row.status == 6 || scope.row.status == 8 || scope.row.status == 4"
+          @confirm="changeStatus(scope.row.creativeId, 10, '启用成功')"
+        >
+          <template #reference>
+            <el-button link type="primary" size="small">启用</el-button>
+          </template>
+        </el-popconfirm>
+        <el-popconfirm
+          title="确定禁用该素材吗?"
+          v-if="scope.row.status == 10"
+          @confirm="changeStatus(scope.row.creativeId, 6, '禁用成功')"
+        >
+          <template #reference>
+            <el-button link type="primary" size="small">禁用</el-button>
+          </template>
+        </el-popconfirm>
+        <el-button link type="primary" size="small" @click="() => creatEditPlan(scope.row)">
+          修改
+        </el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+  <br />
+  <el-pagination
+    background
+    layout="prev, pager, next"
+    :total="pages.total"
+    @current-change="page"
+  />
+
+  <el-dialog v-model="dialogVisible" title="素材">
+    <el-form ref="ruleFormRef" :rules="rules" :model="form" label-width="auto">
+      <el-form-item label="素材名称" prop="stuffName">
+        <el-input v-model="form.stuffName" />
+      </el-form-item>
+      <el-form-item label="选择创意" prop="creativeId">
+        <el-select filterable v-model="form.creativeId" placeholder="选择创意">
+          <el-option
+            v-for="item in originalityList"
+            :key="item.creativeId"
+            :label="item.creativeName"
+            :value="item.creativeId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="跳转地址" prop="landingPage">
+        <el-input v-model="form.landingPage" />
+      </el-form-item>
+      <el-form-item label="上传素材" prop="addr">
+        <MediaComponents
+          v-if="dialogVisible"
+          :src="form.addr || ''"
+          @change="(val: string) => (form.addr = val)"
+          @getCalc="getCalc"
+        />
+      </el-form-item>
+      <el-form-item label="宽(px)" prop="width">
+        <el-input-number v-model="form.width" :min="0" />
+      </el-form-item>
+      <el-form-item label="高(px)" prop="height">
+        <el-input-number v-model="form.height" :min="0" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="creatMaterial(ruleFormRef)">确定</el-button>
+      </el-form-item>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import type { FormInstance, FormRules } from 'element-plus'
+import type { Pages } from '@/types/Tool'
+import type { OriginalityList } from '@/types/AdListPage'
+import type {
+  SourceMaterialList,
+  ISourceMaterialProps,
+  SourceMaterialItem,
+} from '@/types/SourceMaterial'
+import { formatDateSite, getStatus } from '@/tool/index'
+import { ElMessage } from 'element-plus'
+import { useRouter } from 'vue-router'
+import {
+  getMaterialList,
+  deleteMaterial,
+  getOriginalityList,
+  createMaterial,
+  updateMaterial,
+  updateMaterialStatus,
+} from '@/api/index'
+import { ref, reactive } from 'vue'
+
+import MediaComponents from '@/components/mediaComponents.vue'
+
+// vue3 获取url参数
+const router = useRouter()
+const query = router.currentRoute.value.query
+console.log(query.creativeId)
+
+const ruleFormRef = ref<FormInstance>()
+const rules = reactive<FormRules<ISourceMaterialProps>>({
+  stuffName: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+  landingPage: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+  creativeId: [{ required: true, message: '请选择创意', trigger: 'change' }],
+  addr: [{ required: true, message: '请选择素材', trigger: 'change' }],
+  width: [{ required: true, message: '请输入宽', trigger: 'change' }],
+  height: [{ required: true, message: '请输入高', trigger: 'change' }],
+})
+const originalityList = ref<OriginalityList>([])
+getOriginalityList({
+  pageNum: 1,
+  pageSize: 999999,
+}).then(({ data }) => {
+  originalityList.value = data.records || {}
+})
+
+const dialogVisible = ref(false)
+const form = ref<ISourceMaterialProps>({
+  width: 0,
+  height: 0,
+})
+
+const pages = reactive<Pages>({
+  pageNum: 1,
+  pageSize: 10,
+  creativeId: query.creativeId ? Number(query.creativeId) : undefined,
+})
+
+const tableData = ref<SourceMaterialList>([])
+
+const OriginalityName = (id: number) => {
+  const item = originalityList.value.find((item) => item.creativeId === id)
+  return item?.creativeName
+}
+
+const onSubmit = () => {
+  getMaterialList({
+    creativeId: pages.creativeId,
+    keyword: pages.keyword,
+    status: pages.status,
+    pageNum: pages.pageNum,
+    pageSize: pages.pageSize,
+  }).then(({ data }) => {
+    tableData.value = data.records
+    pages.total = data.total
+  })
+}
+
+const creatMaterial = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.validate((valid) => {
+    if (!valid) return
+    if (typeof form.value.stuffId === 'number') {
+      updateMaterial(form.value).then(() => {
+        ElMessage({
+          message: '更新成功',
+          type: 'success',
+        })
+        onSubmit()
+        dialogVisible.value = false
+      })
+      return
+    }
+    createMaterial(form.value).then(() => {
+      ElMessage({
+        message: '创建成功',
+        type: 'success',
+      })
+      onSubmit()
+      dialogVisible.value = false
+    })
+  })
+}
+
+const page = (val: number) => {
+  pages.pageNum = val
+  onSubmit()
+}
+
+const getCalc = (width: number, height: number) => {
+  form.value.width = width
+  form.value.height = height
+}
+
+const creatEditPlan = (data?: SourceMaterialItem) => {
+  if (data != undefined) {
+    form.value = {
+      creativeId: data.creativeId,
+      stuffName: data.stuffName,
+      addr: data.addr,
+      landingPage: data.landingPage,
+      width: data.width,
+      height: data.height,
+      type: data.width,
+      stuffId: data.stuffId,
+    }
+  } else {
+    form.value = {
+      creativeId: pages.creativeId ? pages.creativeId : undefined,
+      stuffName: '',
+      addr: '',
+      landingPage: '',
+      width: 0,
+      height: 0,
+      type: 0,
+      stuffId: undefined,
+    }
+  }
+  dialogVisible.value = !dialogVisible.value
+}
+
+const changeStatus = (id: number, status: number, text: string) => {
+  updateMaterialStatus({ id, status }).then(() => {
+    ElMessage.success(text)
+    onSubmit()
+  })
+}
+
+const deleteMaterialFunc = (id: number) => {
+  deleteMaterial(id).then(() => {
+    ElMessage.success('删除成功')
+    onSubmit()
+  })
+}
+
+onSubmit()
+</script>
+
+<style></style>

+ 14 - 0
tsconfig.app.json

@@ -0,0 +1,14 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+  "exclude": ["src/**/__tests__/*"],
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  }
+}

+ 14 - 0
tsconfig.json

@@ -0,0 +1,14 @@
+{
+  "files": [],
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    },
+    {
+      "path": "./tsconfig.app.json"
+    }
+  ],
+  "compilerOptions": {
+    "types": ["element-plus/global", "AdListPage", "./src/types"]
+  }
+}

+ 19 - 0
tsconfig.node.json

@@ -0,0 +1,19 @@
+{
+  "extends": "@tsconfig/node20/tsconfig.json",
+  "include": [
+    "vite.config.*",
+    "vitest.config.*",
+    "cypress.config.*",
+    "nightwatch.conf.*",
+    "playwright.config.*"
+  ],
+  "compilerOptions": {
+    "composite": true,
+    "noEmit": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "types": ["node"]
+  }
+}

+ 25 - 0
vite.config.ts

@@ -0,0 +1,25 @@
+import { fileURLToPath, URL } from 'node:url'
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),
+    AutoImport({
+      resolvers: [ElementPlusResolver()],
+    }),
+    Components({
+      resolvers: [ElementPlusResolver()],
+    }),
+  ],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url)),
+    },
+  },
+})