Переглянути джерело

fix: 添加颜色选择组件添加页面、标题配置

liaojiaxing 10 місяців тому
батько
коміт
8fe47fcef7

+ 2 - 0
package.json

@@ -15,10 +15,12 @@
     "ant-design-vue": "4.x",
     "dayjs": "^1.11.11",
     "echarts": "^5.5.0",
+    "element-plus": "^2.7.6",
     "less": "^4.2.0",
     "less-loader": "^12.2.0",
     "lodash": "^4.17.21",
     "pinia": "^2.1.7",
+    "unplugin-element-plus": "^0.8.0",
     "vue": "^3.4.21",
     "vue-router": "^4.3.3",
     "vuedraggable": "^4.1.0"

+ 275 - 2
pnpm-lock.yaml

@@ -23,6 +23,9 @@ dependencies:
   echarts:
     specifier: ^5.5.0
     version: 5.5.0
+  element-plus:
+    specifier: ^2.7.6
+    version: 2.7.6(vue@3.4.27)
   less:
     specifier: ^4.2.0
     version: 4.2.0
@@ -35,6 +38,9 @@ dependencies:
   pinia:
     specifier: ^2.1.7
     version: 2.1.7(typescript@5.4.5)(vue@3.4.27)
+  unplugin-element-plus:
+    specifier: ^0.8.0
+    version: 0.8.0
   vue:
     specifier: ^3.4.21
     version: 3.4.27(typescript@5.4.5)
@@ -119,6 +125,14 @@ packages:
     engines: {node: '>=10'}
     dev: false
 
+  /@element-plus/icons-vue@2.3.1(vue@3.4.27):
+    resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==}
+    peerDependencies:
+      vue: ^3.2.0
+    dependencies:
+      vue: 3.4.27(typescript@5.4.5)
+    dev: false
+
   /@emotion/hash@0.9.1:
     resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==}
     dev: false
@@ -334,9 +348,40 @@ packages:
     dev: true
     optional: true
 
+  /@floating-ui/core@1.6.2:
+    resolution: {integrity: sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==}
+    dependencies:
+      '@floating-ui/utils': 0.2.2
+    dev: false
+
+  /@floating-ui/dom@1.6.5:
+    resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==}
+    dependencies:
+      '@floating-ui/core': 1.6.2
+      '@floating-ui/utils': 0.2.2
+    dev: false
+
+  /@floating-ui/utils@0.2.2:
+    resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==}
+    dev: false
+
   /@jridgewell/sourcemap-codec@1.4.15:
     resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
 
+  /@rollup/pluginutils@5.1.0:
+    resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
+    engines: {node: '>=14.0.0'}
+    peerDependencies:
+      rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+    peerDependenciesMeta:
+      rollup:
+        optional: true
+    dependencies:
+      '@types/estree': 1.0.5
+      estree-walker: 2.0.2
+      picomatch: 2.3.1
+    dev: false
+
   /@rollup/rollup-android-arm-eabi@4.18.0:
     resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==}
     cpu: [arm]
@@ -481,9 +526,22 @@ packages:
       nanopop: 2.4.2
     dev: false
 
+  /@sxzz/popperjs-es@2.11.7:
+    resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
+    dev: false
+
   /@types/estree@1.0.5:
     resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
-    dev: true
+
+  /@types/lodash-es@4.17.12:
+    resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
+    dependencies:
+      '@types/lodash': 4.17.5
+    dev: false
+
+  /@types/lodash@4.17.5:
+    resolution: {integrity: sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==}
+    dev: false
 
   /@types/node@20.14.2:
     resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==}
@@ -491,6 +549,10 @@ packages:
       undici-types: 5.26.5
     dev: true
 
+  /@types/web-bluetooth@0.0.16:
+    resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
+    dev: false
+
   /@types/web-bluetooth@0.0.20:
     resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
     dev: false
@@ -647,6 +709,18 @@ packages:
       - vue
     dev: false
 
+  /@vueuse/core@9.13.0(vue@3.4.27):
+    resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
+    dependencies:
+      '@types/web-bluetooth': 0.0.16
+      '@vueuse/metadata': 9.13.0
+      '@vueuse/shared': 9.13.0(vue@3.4.27)
+      vue-demi: 0.14.8(vue@3.4.27)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+    dev: false
+
   /@vueuse/metadata@10.10.1:
     resolution: {integrity: sha512-dpEL5afVLUqbchwGiLrV6spkl4/6UOKJ3YgxFE+wWLj/LakyIZUC83bfeFgbHkRcNhsAqTQCGR74jImsLfK8pg==}
     dev: false
@@ -655,6 +729,10 @@ packages:
     resolution: {integrity: sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==}
     dev: false
 
+  /@vueuse/metadata@9.13.0:
+    resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
+    dev: false
+
   /@vueuse/shared@10.10.1(vue@3.4.27):
     resolution: {integrity: sha512-edqexI+RQpoeqDxTatqBZa+K87ganbrwpoP++Fd9828U3js5jzwcEDeyrYcUgkKZ5LLL8q7M5SOMvSpMrxBPxg==}
     dependencies:
@@ -673,6 +751,21 @@ packages:
       - vue
     dev: false
 
+  /@vueuse/shared@9.13.0(vue@3.4.27):
+    resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
+    dependencies:
+      vue-demi: 0.14.8(vue@3.4.27)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+    dev: false
+
+  /acorn@8.12.0:
+    resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+    dev: false
+
   /ant-design-vue@4.2.3(vue@3.4.27):
     resolution: {integrity: sha512-kqGyWvZtFlSInFP93Ow6wS8LzEsxxUgpI+ZY5jQQkuX8WAcqdwXCA7IcHMpECW6JB89DZMo2Bw85jUg2SjlgQA==}
     engines: {node: '>=12.22.0'}
@@ -704,6 +797,14 @@ packages:
       warning: 4.0.3
     dev: false
 
+  /anymatch@3.1.3:
+    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+    engines: {node: '>= 8'}
+    dependencies:
+      normalize-path: 3.0.0
+      picomatch: 2.3.1
+    dev: false
+
   /array-tree-filter@2.1.0:
     resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==}
     dev: false
@@ -716,12 +817,39 @@ packages:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
     dev: true
 
+  /binary-extensions@2.3.0:
+    resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+    engines: {node: '>=8'}
+    dev: false
+
   /brace-expansion@2.0.1:
     resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
     dependencies:
       balanced-match: 1.0.2
     dev: true
 
+  /braces@3.0.3:
+    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+    engines: {node: '>=8'}
+    dependencies:
+      fill-range: 7.1.1
+    dev: false
+
+  /chokidar@3.6.0:
+    resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+    engines: {node: '>= 8.10.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
+    dev: false
+
   /compute-scroll-into-view@1.0.20:
     resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
     dev: false
@@ -766,6 +894,31 @@ packages:
       zrender: 5.5.0
     dev: false
 
+  /element-plus@2.7.6(vue@3.4.27):
+    resolution: {integrity: sha512-36sw1K23hYjgeooR10U6CiCaCp2wvOqwoFurADZVlekeQ9v5U1FhJCFGEXO6i/kZBBMwsE1c9fxjLs9LENw2Rg==}
+    peerDependencies:
+      vue: ^3.2.0
+    dependencies:
+      '@ctrl/tinycolor': 3.6.1
+      '@element-plus/icons-vue': 2.3.1(vue@3.4.27)
+      '@floating-ui/dom': 1.6.5
+      '@popperjs/core': /@sxzz/popperjs-es@2.11.7
+      '@types/lodash': 4.17.5
+      '@types/lodash-es': 4.17.12
+      '@vueuse/core': 9.13.0(vue@3.4.27)
+      async-validator: 4.2.5
+      dayjs: 1.11.11
+      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.4.27(typescript@5.4.5)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+    dev: false
+
   /entities@4.5.0:
     resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
     engines: {node: '>=0.12'}
@@ -778,6 +931,10 @@ packages:
       prr: 1.0.1
     optional: true
 
+  /es-module-lexer@1.5.3:
+    resolution: {integrity: sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==}
+    dev: false
+
   /esbuild@0.20.2:
     resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==}
     engines: {node: '>=12'}
@@ -809,17 +966,34 @@ packages:
       '@esbuild/win32-x64': 0.20.2
     dev: true
 
+  /escape-html@1.0.3:
+    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+    dev: false
+
   /estree-walker@2.0.2:
     resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
 
+  /fill-range@7.1.1:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+    engines: {node: '>=8'}
+    dependencies:
+      to-regex-range: 5.0.1
+    dev: false
+
   /fsevents@2.3.3:
     resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
     os: [darwin]
     requiresBuild: true
-    dev: true
     optional: true
 
+  /glob-parent@5.1.2:
+    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+    engines: {node: '>= 6'}
+    dependencies:
+      is-glob: 4.0.3
+    dev: false
+
   /graceful-fs@4.2.11:
     resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
     requiresBuild: true
@@ -845,6 +1019,30 @@ packages:
     requiresBuild: true
     optional: true
 
+  /is-binary-path@2.1.0:
+    resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+    engines: {node: '>=8'}
+    dependencies:
+      binary-extensions: 2.3.0
+    dev: false
+
+  /is-extglob@2.1.1:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+    engines: {node: '>=0.10.0'}
+    dev: false
+
+  /is-glob@4.0.3:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+    engines: {node: '>=0.10.0'}
+    dependencies:
+      is-extglob: 2.1.1
+    dev: false
+
+  /is-number@7.0.0:
+    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+    engines: {node: '>=0.12.0'}
+    dev: false
+
   /is-plain-object@3.0.1:
     resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
     engines: {node: '>=0.10.0'}
@@ -894,6 +1092,18 @@ packages:
     resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
     dev: false
 
+  /lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
+    resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
+    peerDependencies:
+      '@types/lodash-es': '*'
+      lodash: '*'
+      lodash-es: '*'
+    dependencies:
+      '@types/lodash-es': 4.17.12
+      lodash: 4.17.21
+      lodash-es: 4.17.21
+    dev: false
+
   /lodash@4.17.21:
     resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
     dev: false
@@ -919,6 +1129,10 @@ packages:
       semver: 5.7.2
     optional: true
 
+  /memoize-one@6.0.0:
+    resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+    dev: false
+
   /mime@1.6.0:
     resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
     engines: {node: '>=4'}
@@ -956,6 +1170,15 @@ packages:
       sax: 1.4.1
     optional: true
 
+  /normalize-path@3.0.0:
+    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+    engines: {node: '>=0.10.0'}
+    dev: false
+
+  /normalize-wheel-es@1.2.0:
+    resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
+    dev: false
+
   /parse-node-version@1.0.1:
     resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==}
     engines: {node: '>= 0.10'}
@@ -967,6 +1190,11 @@ packages:
   /picocolors@1.0.1:
     resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
 
+  /picomatch@2.3.1:
+    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+    engines: {node: '>=8.6'}
+    dev: false
+
   /pify@4.0.1:
     resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
     engines: {node: '>=6'}
@@ -1004,6 +1232,13 @@ packages:
     requiresBuild: true
     optional: true
 
+  /readdirp@3.6.0:
+    resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+    engines: {node: '>=8.10.0'}
+    dependencies:
+      picomatch: 2.3.1
+    dev: false
+
   /regenerator-runtime@0.14.1:
     resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
     dev: false
@@ -1097,6 +1332,13 @@ packages:
     resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
     engines: {node: '>=4'}
 
+  /to-regex-range@5.0.1:
+    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+    engines: {node: '>=8.0'}
+    dependencies:
+      is-number: 7.0.0
+    dev: false
+
   /tslib@2.3.0:
     resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
     dev: false
@@ -1113,6 +1355,28 @@ packages:
     resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
     dev: true
 
+  /unplugin-element-plus@0.8.0:
+    resolution: {integrity: sha512-jByUGY3FG2B8RJKFryqxx4eNtSTj+Hjlo8edcOdJymewndDQjThZ1pRUQHRjQsbKhTV2jEctJV7t7RJ405UL4g==}
+    engines: {node: '>=14.19.0'}
+    dependencies:
+      '@rollup/pluginutils': 5.1.0
+      es-module-lexer: 1.5.3
+      magic-string: 0.30.10
+      unplugin: 1.10.1
+    transitivePeerDependencies:
+      - rollup
+    dev: false
+
+  /unplugin@1.10.1:
+    resolution: {integrity: sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg==}
+    engines: {node: '>=14.0.0'}
+    dependencies:
+      acorn: 8.12.0
+      chokidar: 3.6.0
+      webpack-sources: 3.2.3
+      webpack-virtual-modules: 0.6.2
+    dev: false
+
   /vite@5.2.13(@types/node@20.14.2)(less@4.2.0):
     resolution: {integrity: sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==}
     engines: {node: ^18.0.0 || >=20.0.0}
@@ -1237,6 +1501,15 @@ packages:
       loose-envify: 1.4.0
     dev: false
 
+  /webpack-sources@3.2.3:
+    resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
+    engines: {node: '>=10.13.0'}
+    dev: false
+
+  /webpack-virtual-modules@0.6.2:
+    resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+    dev: false
+
   /zrender@5.5.0:
     resolution: {integrity: sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==}
     dependencies:

+ 6 - 1
src/App.vue

@@ -4,13 +4,18 @@ import zhCN from 'ant-design-vue/es/locale/zh_CN'
 import dayjs from 'dayjs'
 import 'dayjs/locale/zh-cn'
 
+import { ElConfigProvider } from 'element-plus'
+import zhcn from 'element-plus/es/locale/lang/zh-cn'
+
 dayjs.locale('zh-cn')
 
 </script>
 
 <template>
   <ConfigProvider :locale="zhCN">
-    <router-view />
+    <ElConfigProvider :locale="zhcn">
+      <router-view />
+    </ElConfigProvider>
   </ConfigProvider>
 </template>
 

+ 4 - 2
src/components/CusForm/index.ts

@@ -1,5 +1,7 @@
 import CusForm from './src/index.vue';
+import type { IFormItem } from './src/type';
 
-export default {
-  CusForm
+export {
+  CusForm,
+  IFormItem
 }

+ 92 - 27
src/components/CusForm/src/BackgroundSelect.vue

@@ -1,48 +1,113 @@
 <template>
-  <div>
-    <Select v-model="background.type" :options="options" style="width: 100%" />
-    <template v-if="background.type === 'color'">
-      <ColorPicker v-model="background.color" />
-    </template>
-    <template v-else-if="background.type === 'image'">
-      <div class="img-preview">
-        <Image :src="background.image" />
-        <div class="img-tip">选择图片</div>
-      </div>
-      <RadioGroup v-model="background.fillType">
-        <RadioButton value="cover">填充</RadioButton>
-        <RadioButton value="contain">适应</RadioButton>
-        <RadioButton value="stretch">拉伸</RadioButton>
-      </RadioGroup>
-    </template>
-  </div>
+  <Select v-model:value="backgroundObj.type" style="width: 100%" :options="options" @change="handleChangeColor"/>
+  <template v-if="backgroundObj.type === 'color'">
+    <div class="color-box">
+      <ElColorPicker v-model="backgroundObj.color" color-format="hex" show-alpha size="small"/>
+      <ElInput v-model="backgroundObj.color" size="small"></ElInput>
+    </div>
+  </template>
+  <template v-else-if="backgroundObj.type === 'image'">
+    <div class="img-preview">
+      <div class="img-empty">未选择</div>
+      <Image :src="backgroundObj.image" />
+      <div class="img-tip">选择素材</div>
+    </div>
+    <ElRadioGroup v-model="backgroundObj.fillType">
+      <ElRadioButton value="cover">填充</ElRadioButton>
+      <ElRadioButton value="contain">适应</ElRadioButton>
+      <ElRadioButton value="stretch">拉伸</ElRadioButton>
+    </ElRadioGroup>
+  </template>
 </template>
 
 <script setup lang="ts">
-import { defineEmits, defineProps, computed } from "vue";
-import { Select, Image, RadioGroup, RadioButton } from "ant-design-vue";
+import { defineEmits, defineProps, withDefaults, ref, watch } from "vue";
+import { Select, Image } from "ant-design-vue";
+import { ElColorPicker, ElRadioGroup, ElRadioButton, ElInput } from "element-plus";
 
-const props = defineProps<{
+interface Prop {
   background: {
     type: "none" | "color" | "image";
     color?: string;
     image?: string;
-    fillType?: "cover" | "contain" | "stretch";
+    fillType?: "cover" | "contain" | "stretch" | "";
   };
-}>();
+}
+const props = withDefaults(defineProps<Prop>(), {
+  background: () => ({
+    type: "none",
+    color: "",
+    image: "",
+    fillType: ""
+  })
+});
 
 const emit = defineEmits(["update:background"]);
 
-const background = computed({
-  get: () => props.background,
-  set: (val) => emit("update:background", val),
-});
+const backgroundObj = ref(props.background);
 
 const options = [
   { label: "无", value: "none" },
   { label: "颜色", value: "color" },
   { label: "图片", value: "image" },
 ];
+
+watch(
+  () => backgroundObj.value,
+  () => {
+    emit("update:background", backgroundObj.value);
+  }, {
+    deep: true,
+    immediate: true
+  }
+)
+
+const handleChangeColor = (type: any) => {
+  if(type === 'color' && !backgroundObj.value.color) {
+    backgroundObj.value.color = '#0B074BFF';
+  }
+};
 </script>
 
-<style scoped></style>
+<style lang="less" scoped>
+.color-box {
+  display: flex;
+  align-items: center;
+  margin: 12px 0;
+  ::v-deep .el-color-picker {
+    margin-right: 8px;
+  }
+}
+
+.img-preview {
+  margin: 12px 0;
+  border: solid 1px #eee;
+  height: 120px;
+  position: relative;
+  .img-empty {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    color: #999;
+  }
+  .img-tip {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    left: 0;
+    top: 0;
+    background: rgba(0, 0, 0, .6);
+    color: #fff;
+    font-size: 12px;
+    opacity: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    &:hover {
+      opacity: 1;
+    }
+  }
+}
+</style>

+ 62 - 0
src/components/CusForm/src/ColorSelect.vue

@@ -0,0 +1,62 @@
+<template>
+  <ElRadioGroup v-model="colorType" size="small">
+    <ElRadioButton value="pure">单色</ElRadioButton>
+    <ElRadioButton value="gradient">渐变色</ElRadioButton>
+  </ElRadioGroup>
+  <template v-if="colorType === 'pure'">
+    <div class="color-box">
+      <ElColorPicker v-model="color" color-format="hex" show-alpha size="small"/>
+      <ElInput v-model="color" size="small"></ElInput>
+    </div>
+  </template>
+  <template v-else-if="colorType === 'gradient'">
+    <div class="gradient-box" :style="{ background: color }">
+      <ElColorPicker v-model="gradientColor[0]" color-format="hex" show-alpha size="small"/>
+      <ElColorPicker v-model="gradientColor[1]" color-format="hex" show-alpha size="small"/>
+    </div>
+  </template>
+</template>
+
+<script setup lang="ts">
+import { ElRadioGroup, ElRadioButton, ElColorPicker, ElInput, } from 'element-plus';
+import { defineEmits, defineProps, ref, watch } from 'vue';
+
+const emit = defineEmits(["update:value"]);
+const props = defineProps<{
+  value: string;
+}>();
+const colorType = ref(props.value?.length <= 7 || !props.value ? 'pure' : 'gradient');
+const color = ref(props.value);
+const gradientColor = ref(props.value.length >= 7 ? props.value?.split(',') : ['#4ba9ff', '#fff']);
+
+watch(
+  () => [colorType.value, color.value, gradientColor.value],
+  () => {
+    if (colorType.value === 'pure') {
+      color.value = color.value.length > 9 ? '#FFFFFFFF' : color.value;
+    } else {
+      color.value = `linear-gradient(90deg, ${gradientColor.value.join(',')})`
+    }
+    emit('update:value', color.value);
+  }
+);
+</script>
+
+<style lang="less" scoped>
+.color-box {
+  display: flex;
+  align-items: center;
+  margin: 12px 0;
+  ::v-deep .el-color-picker {
+    margin-right: 8px;
+  }
+}
+.gradient-box {
+  padding: 2px;
+  margin: 12px 0;
+  border: solid 1px #eee;
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+}
+</style>

+ 50 - 48
src/components/CusForm/src/index.vue

@@ -1,34 +1,41 @@
 <template>
-  <Form :model="formModel" ref="formRef" layout="horizontal">
+  <Form
+    :model="formModel"
+    :colon="false"
+    :label-col="{ span: 6 }"
+    ref="formRef"
+    layout="horizontal"
+    size="small"
+  >
     <FormItem
       v-for="item in formItems"
-      :key="item.key"
+      :key="item.prop"
       :label="item.label"
-      :name="item.key"
+      :name="item.prop"
       :rules="item.rules"
     >
       <template v-if="item.type === 'input'">
-        <Input v-model="formModel[item.key]" />
+        <Input v-model:value="formModel[item.prop]" />
       </template>
       <template v-else-if="item.type === 'select'">
-        <Select v-model="formModel[item.key]" :options="item.options" />
+        <Select v-model:value="formModel[item.prop]" :options="item.options" />
       </template>
       <template v-else-if="item.type === 'inputnumber'">
-        <InputNumber v-model="formModel[item.key]" />
+        <InputNumber v-model:value="formModel[item.prop]" />
       </template>
       <template v-else-if="item.type === 'image'">
-        <Image v-model="formModel[item.key]" />
+        <Image v-model:value="formModel[item.prop]" />
       </template>
       <template v-else-if="item.type === 'checkbox'">
-        <Checkbox v-model="formModel[item.key]" />
+        <Checkbox v-model:value="formModel[item.prop]" />
       </template>
       <template v-else-if="item.type === 'backgroundSelect'">
-        <BackgroundSelect v-model:background="formModel[item.key]" />
+        <BackgroundSelect v-model:background="formModel[item.prop]" />
       </template>
-      <!-- <template v-else-if="item.type === 'boderSelect'">
-        <BoderSelect v-model="formModel[item.key]" />
+      <template v-else-if="item.type === 'colorSelect'">
+        <ColorSelect v-model:value="formModel[item.prop]" />
       </template>
-      <template v-else-if="item.type === 'boderRadiusSelect'">
+      <!--<template v-else-if="item.type === 'boderRadiusSelect'">
         <BoderRadiusSelect v-model="formModel[item.key]" />
       </template>
       <template v-else-if="item.type === 'shodowSelect'">
@@ -48,8 +55,16 @@
 </template>
 
 <script setup lang="ts">
+import type { IFormItem } from "./type";
 import type { FormInstance } from "ant-design-vue";
-import { ref, defineProps, defineExpose, computed, watch } from "vue";
+import {
+  ref,
+  defineProps,
+  defineExpose,
+  computed,
+  watch,
+  defineEmits,
+} from "vue";
 import {
   Form,
   FormItem,
@@ -59,36 +74,17 @@ import {
   Checkbox,
   Image,
 } from "ant-design-vue";
-import BackgroundSelect from "./BackgroundSelect.vue";
 
-interface FormItem {
-  label: string;
-  key: string;
-  type:
-    | "input"
-    | "select"
-    | "inputnumber"
-    | "image"
-    | "checkbox"
-    | "backgroundSelect" // 背景选择
-    | "boderSelect" // 边框选择
-    | "boderRadiusSelect" // 边框圆角选择
-    | "shodowSelect" // 阴影选择
-    | "paddingSelect" // 内边距选择
-    | "rotateSelect" // 旋转选择
-    | "opacitySelect"; // 透明度选择
-  options?: any[];
-  prefix?: string;
-  suffix?: string;
-  rules?: any[];
-  defaultValue?: any;
-}
+import BackgroundSelect from "./BackgroundSelect.vue";
+import ColorSelect from "./ColorSelect.vue";
 
 const props = defineProps<{
-  columns: FormItem[];
-  formModel: Record<string, any>;
+  columns: IFormItem[];
+  formModel?: Record<string, any>;
 }>();
 
+const emit = defineEmits(["change"]);
+
 const formModel = ref<Record<string, any>>({});
 const formRef = ref<FormInstance>();
 
@@ -102,19 +98,25 @@ const formItems = computed(() => {
 });
 
 watch(
-  () => props.columns,
-  () => {
-    props.columns.forEach((item) => {
-      formModel.value[item.key] = item?.defaultValue;
-      if (props.formModel[item.key] !== undefined) {
-        formModel.value[item.key] = props.formModel[item.key];
-      }
-    });
+  () => formItems.value,
+  (val) => {
+    val &&
+      props.columns?.forEach((item) => {
+        formModel.value[item.prop] = item?.defaultValue;
+      });
   },
   { immediate: true }
-)
+);
+
+watch(
+  () => formModel.value,
+  (val) => {
+    emit("change", val);
+  },
+  { deep: true }
+);
 
 defineExpose(formRef.value);
 </script>
 
-<style scoped></style>
+<style lang="less" scoped></style>

+ 24 - 0
src/components/CusForm/src/type.ts

@@ -0,0 +1,24 @@
+export interface IFormItem {
+  label: string;
+  icon?: string;
+  prop: string;
+  type:
+    | "input"
+    | "select"
+    | "inputnumber"
+    | "image"
+    | "checkbox"
+    | "backgroundSelect" // 背景选择
+    | "boderSelect" // 边框选择
+    | "boderRadiusSelect" // 边框圆角选择
+    | "shodowSelect" // 阴影选择
+    | "paddingSelect" // 内边距选择
+    | "rotateSelect" // 旋转选择
+    | "opacitySelect" // 透明度选择
+    | "colorSelect" // 颜色选择
+  options?: any[];
+  prefix?: string;
+  suffix?: string;
+  rules?: any[];
+  defaultValue?: any;
+}

+ 3 - 1
src/components/Text/Title/index.ts

@@ -1,4 +1,6 @@
 import Title from './src/index.vue';
-export default Title;
+import Config from './src/Config.vue';
 
+export default Title;
+export { Config };
 export { defaultPropsValue, titleProps } from './src/props';

+ 43 - 0
src/components/Text/Title/src/Config.vue

@@ -0,0 +1,43 @@
+<!-- 组件自定义配置部分 -->
+<template>
+  <CusForm :columns="columns" v-bind="$attrs" @change="handleChange">
+  </CusForm>
+</template>
+
+<script setup lang="ts">
+import { defineProps, defineEmits, computed } from 'vue';
+import { CusForm, type IFormItem } from '@/components/CusForm';
+import { defaultPropsValue, titleProps } from './props';
+
+const props = defineProps(titleProps);
+const emit = defineEmits(["change"]);
+
+const columns = computed((): IFormItem[] => [
+  {
+    label: '内容',
+    prop: 'text',
+    type: 'input',
+    defaultValue: props.text,
+  },
+  {
+    label: '字体',
+    prop: 'fontSize',
+    type: 'inputnumber',
+    defaultValue: props.fontSize,
+  },
+  {
+    label: '颜色',
+    prop: 'color',
+    type: 'colorSelect',
+    defaultValue: props.color,
+  }
+]);
+
+const handleChange = (val: Record<string, any>) => {
+  emit('change', val);
+};
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 7 - 1
src/components/Text/Title/src/index.vue

@@ -12,8 +12,14 @@ import { transformStyle } from '@/utils/style';
 const props = defineProps(titleProps);
 const style = computed(() => {
   const style = transformStyle(props);
-
+  const obj: Record<string, string> = {};
+  if(style.color.length > 9) {
+    obj.backgroundImage = style.color;
+    obj.webkitBackgroundClip = 'text';
+    obj.webkitTextFillColor = 'transparent';
+  }
   return {
+    ...obj,
     ...style,
     width: '100%',
     height: '100%',

+ 11 - 2
src/store/modules/project.ts

@@ -1,4 +1,4 @@
-import type { ProjectInfo, Page, ReferLine, CustomElement } from "#/project";
+import type { ProjectInfo, Page, ReferLine } from "#/project";
 import type { ComponentType } from "@/components";
 import { defineStore } from "pinia";
 import componentAll from "@/components";
@@ -25,7 +25,7 @@ const defaultPage: Page = {
   name: "页面1",
   background: {
     type: "color",
-    color: "#0b074b",
+    color: "#0B074BFF",
     image: "",
     fillType: "",
   },
@@ -66,6 +66,11 @@ export const useProjectStore = defineStore({
     currentPage(state) {
       return state.projectInfo.pages[state.activePageIndex];
     },
+    currentSelectedElements(state) {
+      return state.projectInfo.pages[state.activePageIndex].elements.filter(
+        (item) => state.selectedElementKeys.includes(item.key)
+      );
+    }
   },
   actions: {
     setProjectInfo(info: any) {
@@ -183,5 +188,9 @@ export const useProjectStore = defineStore({
     clearAllSelectedElement() {
       this.selectedElementKeys = [];
     },
+    // 设置当前页面背景
+    setCurrentPageBackground(background: any) {
+      this.projectInfo.pages[this.activePageIndex].background = background;
+    }
   },
 });

+ 36 - 7
src/views/designer/component/Configurator.vue

@@ -4,15 +4,21 @@
     <Tabs v-if="projectStore.selectedElementKeys.length === 0" centered>
       <TabPane key="1" tab="页面">
         <div class="config-content">
-          <PageConfig/>
+          <PageConfig />
         </div>
       </TabPane>
     </Tabs>
 
     <!-- 组件设置 -->
-    <Tabs  centered v-else>
+    <Tabs centered v-else>
       <TabPane key="1" tab="内容">
-        <div class="config-content">内容配置</div>
+        <div class="config-content">
+          <component
+            :is="configComponent"
+            @change="handleConfigChange"
+            v-bind="projectStore.currentSelectedElements[0].props"
+          />
+        </div>
       </TabPane>
       <TabPane key="2" tab="事件">
         <div class="config-content">事件处理</div>
@@ -28,12 +34,35 @@
 </template>
 
 <script setup lang="ts">
-import { Tabs, TabPane } from 'ant-design-vue';
-import { useProjectStore } from '@/store/modules/project';
-import PageConfig from './PageConfig.vue';
+import { shallowRef, watch } from "vue";
+import { Tabs, TabPane } from "ant-design-vue";
+import { useProjectStore } from "@/store/modules/project";
+import PageConfig from "./PageConfig.vue";
+import componentAll from "@/components";
 
 const projectStore = useProjectStore();
+const configComponent = shallowRef<null | string>(null);
+
+watch(
+  () => projectStore.currentSelectedElements,
+  async (val) => {
+    if (val.length === 1) {
+      const { Config } = await componentAll[
+        val[0].componentType as keyof typeof componentAll
+      ]?.();
+      configComponent.value = Config;
+    } else {
+      configComponent.value = null;
+    }
+  },
+  { immediate: true, deep: true }
+);
 
+const handleConfigChange = (config: any) => {
+  console.log(config);
+  const key = projectStore.selectedElementKeys[0];
+  projectStore.updateElement(key, 'props', config);
+};
 </script>
 
 <style lang="less" scoped>
@@ -45,4 +74,4 @@ const projectStore = useProjectStore();
     padding-top: 0;
   }
 }
-</style>
+</style>

+ 8 - 1
src/views/designer/component/LayerManagement.vue

@@ -6,7 +6,7 @@
         <CloseOutlined />
       </Button>
     </div>
-
+    <div class="line"></div>
     <InputSearch
       allowClear
       size="small"
@@ -108,5 +108,12 @@ const dragEnd = (event: CustomEvent & {newIndex: number}) => {
       border-bottom: dashed 1px #ccc;
     }
   }
+
+  .line {
+    width: calc(100% + 16px);
+    border-top: solid 1px #eee;
+    margin-bottom: 8px;
+    margin-left: -8px;
+  }
 }
 </style>

+ 13 - 7
src/views/designer/component/PageConfig.vue

@@ -1,19 +1,25 @@
 <template>
-  <div>
-    <CusForm :columns="formItems" />
-  </div>
+  <CusForm :columns="formItems" @change="handleChange"/>
 </template>
 
 <script setup lang="ts">
-// import { CusForm } from '@/components/CusForm';
+import { CusForm, IFormItem } from '@/components/CusForm';
+import { useProjectStore } from '@/store/modules/project';
 
-const formItems = [
+const projectStore = useProjectStore();
+const formItems: IFormItem[] = [
   {
     label: '页面背景',
-    key: 'background',
+    prop: 'background',
     type: 'backgroundSelect',
+    defaultValue: projectStore.currentPage.background
   }
-]
+];
+
+const handleChange = (value: Record<string, any>) => {
+  projectStore.setCurrentPageBackground(value.background);
+};
+
 </script>
 
 <style scoped>

+ 2 - 2
types/project.d.ts

@@ -1,13 +1,13 @@
 import type { ComponentType } from "@/components";
 declare interface BackgroundOptions {
   // 背景类型
-  type: 'color' | 'image';
+  type: 'color' | 'image' | 'none';
   // 背景颜色
   color?: string;
   // 背景图片
   image?: string;
   // 背景图片填充方式
-  fillType?: string;
+  fillType?: "cover" | "contain" | "fill" | "";
 }
 
 declare interface CustomElement {

+ 2 - 1
vite.config.ts

@@ -1,10 +1,11 @@
 import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
 import path from 'path';
+import ElementPlus from 'unplugin-element-plus/vite'
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [vue()],
+  plugins: [vue(), ElementPlus()],
   resolve: {
     alias: {
       "@": path.resolve(__dirname, "src"),