seeseele 2 ヶ月 前
コミット
67e9cc65ab
82 ファイル変更14020 行追加0 行削除
  1. 30 0
      .gitignore
  2. 36 0
      README.en.md
  3. 39 0
      README.md
  4. 35 0
      index.html
  5. 8 0
      jsconfig.json
  6. 1321 0
      package-lock.json
  7. 26 0
      package.json
  8. 905 0
      pnpm-lock.yaml
  9. BIN
      public/favicon.ico
  10. 444 0
      src/App.vue
  11. BIN
      src/assets/icon/信息.png
  12. BIN
      src/assets/icon/头像.png
  13. BIN
      src/assets/icon/更多.png
  14. BIN
      src/assets/img/N.png
  15. BIN
      src/assets/img/万里茶道.png
  16. BIN
      src/assets/img/底部背景.png
  17. BIN
      src/assets/img/箭头.png
  18. BIN
      src/assets/img/茶道申遗.png
  19. 171 0
      src/components/Common/ImageUpload.vue
  20. 157 0
      src/components/Common/TablePaging.vue
  21. 175 0
      src/components/Common/VideoUpload.vue
  22. 107 0
      src/components/Common/drawer.vue
  23. 45 0
      src/components/Common/new-drawer.vue
  24. 94 0
      src/components/Common/slideshow.vue
  25. 30 0
      src/http/api/general/jsencrypt.js
  26. 11 0
      src/http/api/general/user.js
  27. 15 0
      src/http/api/pages/Article/index.js
  28. 47 0
      src/http/api/pages/DataSources/index.js
  29. 11 0
      src/http/api/pages/DataType/index.js
  30. 24 0
      src/http/api/pages/Favorite/index.js
  31. 54 0
      src/http/api/pages/Gallery/index.js
  32. 15 0
      src/http/api/pages/Notice/index.js
  33. 55 0
      src/http/api/pages/Order/index.js
  34. 37 0
      src/http/api/pages/Scene/index.js
  35. 37 0
      src/http/api/pages/Spot/index.js
  36. 37 0
      src/http/api/pages/Template/index.js
  37. 76 0
      src/http/api/pages/User/index.js
  38. 8 0
      src/http/config/index.js
  39. 84 0
      src/http/index.js
  40. 26 0
      src/main.js
  41. 183 0
      src/pages/Article/ArticleColumn.vue
  42. 208 0
      src/pages/Article/ArtideContent/ArticleDetails.vue
  43. 349 0
      src/pages/Gallery/showroomCase.vue
  44. 287 0
      src/pages/Gallery/showroomCaseDetail.vue
  45. 181 0
      src/pages/Home/HomeComponents/basic_introduction.vue
  46. 272 0
      src/pages/Home/HomeComponents/head.vue
  47. 302 0
      src/pages/Home/HomeComponents/news_information.vue
  48. 346 0
      src/pages/Home/HomeComponents/recent_project.vue
  49. 192 0
      src/pages/Home/HomeComponents/tail.vue
  50. 244 0
      src/pages/Home/HomeComponents/tea_ceremony.vue
  51. 34 0
      src/pages/Home/index.vue
  52. 376 0
      src/pages/Order/ConfirmOrder.vue
  53. 249 0
      src/pages/Order/OrderSuccess.vue
  54. 256 0
      src/pages/Order/PayOrder.vue
  55. 442 0
      src/pages/Template/templateCase.vue
  56. 395 0
      src/pages/User/UserComponents/CreateEditPage/CreateEditDataSources.vue
  57. 415 0
      src/pages/User/UserComponents/CreateEditPage/CreateEditScene.vue
  58. 219 0
      src/pages/User/UserComponents/CreateEditPage/CreateEditShowroom.vue
  59. 633 0
      src/pages/User/UserComponents/CreateEditPage/CreateEditSpot.vue
  60. 320 0
      src/pages/User/UserComponents/myFavorite.vue
  61. 439 0
      src/pages/User/UserComponents/myOrder.vue
  62. 373 0
      src/pages/User/UserComponents/myScene.vue
  63. 177 0
      src/pages/User/UserComponents/myShoppingCart.vue
  64. 366 0
      src/pages/User/UserComponents/myShowroom.vue
  65. 236 0
      src/pages/User/UserComponents/myTemplate.vue
  66. 354 0
      src/pages/User/UserComponents/notificationCenter.vue
  67. 472 0
      src/pages/User/UserComponents/personalData.vue
  68. 240 0
      src/pages/User/forget.vue
  69. 335 0
      src/pages/User/login.vue
  70. 131 0
      src/pages/User/personalCenter.vue
  71. 261 0
      src/pages/User/register.vue
  72. 83 0
      src/router/index.js
  73. 20 0
      src/store/index.js
  74. 57 0
      src/store/modules/myScene.js
  75. 56 0
      src/store/modules/myShowroom.js
  76. 56 0
      src/store/modules/myTemplate.js
  77. 27 0
      src/store/modules/personalCenter.js
  78. 85 0
      src/store/modules/shoppingCart.js
  79. 42 0
      src/store/modules/showroomCase.js
  80. 56 0
      src/store/modules/templateCase.js
  81. 73 0
      src/store/modules/user.js
  82. 18 0
      vite.config.js

+ 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

+ 36 - 0
README.en.md

@@ -0,0 +1,36 @@
+# vr-one
+
+#### Description
+{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
+
+#### Software Architecture
+Software architecture description
+
+#### Installation
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### Instructions
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### Contribution
+
+1.  Fork the repository
+2.  Create Feat_xxx branch
+3.  Commit your code
+4.  Create Pull Request
+
+
+#### Gitee Feature
+
+1.  You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
+2.  Gitee blog [blog.gitee.com](https://blog.gitee.com)
+3.  Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
+4.  The most valuable open source project [GVP](https://gitee.com/gvp)
+5.  The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
+6.  The most popular members  [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

+ 39 - 0
README.md

@@ -0,0 +1,39 @@
+# vr-one
+
+#### 介绍
+{**以下是 Gitee 平台说明,您可以替换此简介**
+Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台
+无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)}
+
+#### 软件架构
+软件架构说明
+
+
+#### 安装教程
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### 使用说明
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### 参与贡献
+
+1.  Fork 本仓库
+2.  新建 Feat_xxx 分支
+3.  提交代码
+4.  新建 Pull Request
+
+
+#### 特技
+
+1.  使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
+2.  Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
+3.  你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
+4.  [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
+5.  Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
+6.  Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

+ 35 - 0
index.html

@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>VR_ONE展厅</title>
+    <style>
+      body {
+        margin: 0px;
+      }
+      #app {
+        width: 100%;
+        height: 100vh;
+          /* display: flex;
+          flex-direction: column;
+          overflow-y: auto; */
+          overflow-x: hidden;
+      }
+      ::-webkit-scrollbar {
+        width: 6px;
+        height: 6px;
+        background-color: rgba(0, 0, 0, 0.1);
+      }
+      ::-webkit-scrollbar-thumb {
+        border-radius: 10px;
+        background-color: #FD571F;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 8 - 0
jsconfig.json

@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

ファイルの差分が大きいため隠しています
+ 1321 - 0
package-lock.json


+ 26 - 0
package.json

@@ -0,0 +1,26 @@
+{
+  "name": "vr-one-clientside",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
+    "axios": "^1.6.8",
+    "element-plus": "^2.7.3",
+    "js-cookie": "^3.0.5",
+    "jsencrypt": "^3.3.2",
+    "pinia": "^2.1.7",
+    "pinia-plugin-persistedstate": "^3.2.1",
+    "vue": "^3.4.21",
+    "vue-router": "^4.3.0"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.0.4",
+    "vite": "^5.2.8"
+  }
+}

+ 905 - 0
pnpm-lock.yaml

@@ -0,0 +1,905 @@
+lockfileVersion: '6.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+dependencies:
+  axios:
+    specifier: ^1.6.8
+    version: 1.7.2
+  element-plus:
+    specifier: ^2.7.3
+    version: 2.7.7(vue@3.4.33)
+  js-cookie:
+    specifier: ^3.0.5
+    version: 3.0.5
+  jsencrypt:
+    specifier: ^3.3.2
+    version: 3.3.2
+  pinia:
+    specifier: ^2.1.7
+    version: 2.1.7(vue@3.4.33)
+  pinia-plugin-persistedstate:
+    specifier: ^3.2.1
+    version: 3.2.1(pinia@2.1.7)
+  vue:
+    specifier: ^3.4.21
+    version: 3.4.33
+  vue-router:
+    specifier: ^4.3.0
+    version: 4.4.0(vue@3.4.33)
+
+devDependencies:
+  '@vitejs/plugin-vue':
+    specifier: ^5.0.4
+    version: 5.0.5(vite@5.3.4)(vue@3.4.33)
+  vite:
+    specifier: ^5.2.8
+    version: 5.3.4
+
+packages:
+
+  /@babel/helper-string-parser@7.24.8:
+    resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==}
+    engines: {node: '>=6.9.0'}
+
+  /@babel/helper-validator-identifier@7.24.7:
+    resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==}
+    engines: {node: '>=6.9.0'}
+
+  /@babel/parser@7.24.8:
+    resolution: {integrity: sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+    dependencies:
+      '@babel/types': 7.24.9
+
+  /@babel/types@7.24.9:
+    resolution: {integrity: sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/helper-string-parser': 7.24.8
+      '@babel/helper-validator-identifier': 7.24.7
+      to-fast-properties: 2.0.0
+
+  /@ctrl/tinycolor@3.6.1:
+    resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
+    engines: {node: '>=10'}
+    dev: false
+
+  /@element-plus/icons-vue@2.3.1(vue@3.4.33):
+    resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==}
+    peerDependencies:
+      vue: ^3.2.0
+    dependencies:
+      vue: 3.4.33
+    dev: false
+
+  /@esbuild/aix-ppc64@0.21.5:
+    resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [aix]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/android-arm64@0.21.5:
+    resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [android]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/android-arm@0.21.5:
+    resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [android]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/android-x64@0.21.5:
+    resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [android]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/darwin-arm64@0.21.5:
+    resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/darwin-x64@0.21.5:
+    resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/freebsd-arm64@0.21.5:
+    resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [freebsd]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/freebsd-x64@0.21.5:
+    resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [freebsd]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-arm64@0.21.5:
+    resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-arm@0.21.5:
+    resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-ia32@0.21.5:
+    resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-loong64@0.21.5:
+    resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
+    engines: {node: '>=12'}
+    cpu: [loong64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-mips64el@0.21.5:
+    resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
+    engines: {node: '>=12'}
+    cpu: [mips64el]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-ppc64@0.21.5:
+    resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-riscv64@0.21.5:
+    resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
+    engines: {node: '>=12'}
+    cpu: [riscv64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-s390x@0.21.5:
+    resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
+    engines: {node: '>=12'}
+    cpu: [s390x]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-x64@0.21.5:
+    resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/netbsd-x64@0.21.5:
+    resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [netbsd]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/openbsd-x64@0.21.5:
+    resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [openbsd]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/sunos-x64@0.21.5:
+    resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [sunos]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/win32-arm64@0.21.5:
+    resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/win32-ia32@0.21.5:
+    resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/win32-x64@0.21.5:
+    resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@floating-ui/core@1.6.5:
+    resolution: {integrity: sha512-8GrTWmoFhm5BsMZOTHeGD2/0FLKLQQHvO/ZmQga4tKempYRLz8aqJGqXVuQgisnMObq2YZ2SgkwctN1LOOxcqA==}
+    dependencies:
+      '@floating-ui/utils': 0.2.5
+    dev: false
+
+  /@floating-ui/dom@1.6.8:
+    resolution: {integrity: sha512-kx62rP19VZ767Q653wsP1XZCGIirkE09E0QUGNYTM/ttbbQHqcGPdSfWFxUyyNLc/W6aoJRBajOSXhP6GXjC0Q==}
+    dependencies:
+      '@floating-ui/core': 1.6.5
+      '@floating-ui/utils': 0.2.5
+    dev: false
+
+  /@floating-ui/utils@0.2.5:
+    resolution: {integrity: sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==}
+    dev: false
+
+  /@jridgewell/sourcemap-codec@1.5.0:
+    resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+
+  /@rollup/rollup-android-arm-eabi@4.19.0:
+    resolution: {integrity: sha512-JlPfZ/C7yn5S5p0yKk7uhHTTnFlvTgLetl2VxqE518QgyM7C9bSfFTYvB/Q/ftkq0RIPY4ySxTz+/wKJ/dXC0w==}
+    cpu: [arm]
+    os: [android]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-android-arm64@4.19.0:
+    resolution: {integrity: sha512-RDxUSY8D1tWYfn00DDi5myxKgOk6RvWPxhmWexcICt/MEC6yEMr4HNCu1sXXYLw8iAsg0D44NuU+qNq7zVWCrw==}
+    cpu: [arm64]
+    os: [android]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-darwin-arm64@4.19.0:
+    resolution: {integrity: sha512-emvKHL4B15x6nlNTBMtIaC9tLPRpeA5jMvRLXVbl/W9Ie7HhkrE7KQjvgS9uxgatL1HmHWDXk5TTS4IaNJxbAA==}
+    cpu: [arm64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-darwin-x64@4.19.0:
+    resolution: {integrity: sha512-fO28cWA1dC57qCd+D0rfLC4VPbh6EOJXrreBmFLWPGI9dpMlER2YwSPZzSGfq11XgcEpPukPTfEVFtw2q2nYJg==}
+    cpu: [x64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-arm-gnueabihf@4.19.0:
+    resolution: {integrity: sha512-2Rn36Ubxdv32NUcfm0wB1tgKqkQuft00PtM23VqLuCUR4N5jcNWDoV5iBC9jeGdgS38WK66ElncprqgMUOyomw==}
+    cpu: [arm]
+    os: [linux]
+    libc: [glibc]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-arm-musleabihf@4.19.0:
+    resolution: {integrity: sha512-gJuzIVdq/X1ZA2bHeCGCISe0VWqCoNT8BvkQ+BfsixXwTOndhtLUpOg0A1Fcx/+eA6ei6rMBzlOz4JzmiDw7JQ==}
+    cpu: [arm]
+    os: [linux]
+    libc: [musl]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-arm64-gnu@4.19.0:
+    resolution: {integrity: sha512-0EkX2HYPkSADo9cfeGFoQ7R0/wTKb7q6DdwI4Yn/ULFE1wuRRCHybxpl2goQrx4c/yzK3I8OlgtBu4xvted0ug==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-arm64-musl@4.19.0:
+    resolution: {integrity: sha512-GlIQRj9px52ISomIOEUq/IojLZqzkvRpdP3cLgIE1wUWaiU5Takwlzpz002q0Nxxr1y2ZgxC2obWxjr13lvxNQ==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-powerpc64le-gnu@4.19.0:
+    resolution: {integrity: sha512-N6cFJzssruDLUOKfEKeovCKiHcdwVYOT1Hs6dovDQ61+Y9n3Ek4zXvtghPPelt6U0AH4aDGnDLb83uiJMkWYzQ==}
+    cpu: [ppc64]
+    os: [linux]
+    libc: [glibc]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-riscv64-gnu@4.19.0:
+    resolution: {integrity: sha512-2DnD3mkS2uuam/alF+I7M84koGwvn3ZVD7uG+LEWpyzo/bq8+kKnus2EVCkcvh6PlNB8QPNFOz6fWd5N8o1CYg==}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [glibc]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-s390x-gnu@4.19.0:
+    resolution: {integrity: sha512-D6pkaF7OpE7lzlTOFCB2m3Ngzu2ykw40Nka9WmKGUOTS3xcIieHe82slQlNq69sVB04ch73thKYIWz/Ian8DUA==}
+    cpu: [s390x]
+    os: [linux]
+    libc: [glibc]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-x64-gnu@4.19.0:
+    resolution: {integrity: sha512-HBndjQLP8OsdJNSxpNIN0einbDmRFg9+UQeZV1eiYupIRuZsDEoeGU43NQsS34Pp166DtwQOnpcbV/zQxM+rWA==}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-linux-x64-musl@4.19.0:
+    resolution: {integrity: sha512-HxfbvfCKJe/RMYJJn0a12eiOI9OOtAUF4G6ozrFUK95BNyoJaSiBjIOHjZskTUffUrB84IPKkFG9H9nEvJGW6A==}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-win32-arm64-msvc@4.19.0:
+    resolution: {integrity: sha512-HxDMKIhmcguGTiP5TsLNolwBUK3nGGUEoV/BO9ldUBoMLBssvh4J0X8pf11i1fTV7WShWItB1bKAKjX4RQeYmg==}
+    cpu: [arm64]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-win32-ia32-msvc@4.19.0:
+    resolution: {integrity: sha512-xItlIAZZaiG/u0wooGzRsx11rokP4qyc/79LkAOdznGRAbOFc+SfEdfUOszG1odsHNgwippUJavag/+W/Etc6Q==}
+    cpu: [ia32]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@rollup/rollup-win32-x64-msvc@4.19.0:
+    resolution: {integrity: sha512-xNo5fV5ycvCCKqiZcpB65VMR11NJB+StnxHz20jdqRAktfdfzhgjTiJ2doTDQE/7dqGaV5I7ZGqKpgph6lCIag==}
+    cpu: [x64]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@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.7
+    dev: false
+
+  /@types/lodash@4.17.7:
+    resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==}
+    dev: false
+
+  /@types/web-bluetooth@0.0.16:
+    resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
+    dev: false
+
+  /@vitejs/plugin-vue@5.0.5(vite@5.3.4)(vue@3.4.33):
+    resolution: {integrity: sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    peerDependencies:
+      vite: ^5.0.0
+      vue: ^3.2.25
+    dependencies:
+      vite: 5.3.4
+      vue: 3.4.33
+    dev: true
+
+  /@vue/compiler-core@3.4.33:
+    resolution: {integrity: sha512-MoIREbkdPQlnGfSKDMgzTqzqx5nmEjIc0ydLVYlTACGBsfvOJ4tHSbZXKVF536n6fB+0eZaGEOqsGThPpdvF5A==}
+    dependencies:
+      '@babel/parser': 7.24.8
+      '@vue/shared': 3.4.33
+      entities: 4.5.0
+      estree-walker: 2.0.2
+      source-map-js: 1.2.0
+
+  /@vue/compiler-dom@3.4.33:
+    resolution: {integrity: sha512-GzB8fxEHKw0gGet5BKlpfXEqoBnzSVWwMnT+dc25wE7pFEfrU/QsvjZMP9rD4iVXHBBoemTct8mN0GJEI6ZX5A==}
+    dependencies:
+      '@vue/compiler-core': 3.4.33
+      '@vue/shared': 3.4.33
+
+  /@vue/compiler-sfc@3.4.33:
+    resolution: {integrity: sha512-7rk7Vbkn21xMwIUpHQR4hCVejwE6nvhBOiDgoBcR03qvGqRKA7dCBSsHZhwhYUsmjlbJ7OtD5UFIyhP6BY+c8A==}
+    dependencies:
+      '@babel/parser': 7.24.8
+      '@vue/compiler-core': 3.4.33
+      '@vue/compiler-dom': 3.4.33
+      '@vue/compiler-ssr': 3.4.33
+      '@vue/shared': 3.4.33
+      estree-walker: 2.0.2
+      magic-string: 0.30.10
+      postcss: 8.4.39
+      source-map-js: 1.2.0
+
+  /@vue/compiler-ssr@3.4.33:
+    resolution: {integrity: sha512-0WveC9Ai+eT/1b6LCV5IfsufBZ0HP7pSSTdDjcuW302tTEgoBw8rHVHKPbGUtzGReUFCRXbv6zQDDgucnV2WzQ==}
+    dependencies:
+      '@vue/compiler-dom': 3.4.33
+      '@vue/shared': 3.4.33
+
+  /@vue/devtools-api@6.6.3:
+    resolution: {integrity: sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==}
+    dev: false
+
+  /@vue/reactivity@3.4.33:
+    resolution: {integrity: sha512-B24QIelahDbyHipBgbUItQblbd4w5HpG3KccL+YkGyo3maXyS253FzcTR3pSz739OTphmzlxP7JxEMWBpewilA==}
+    dependencies:
+      '@vue/shared': 3.4.33
+
+  /@vue/runtime-core@3.4.33:
+    resolution: {integrity: sha512-6wavthExzT4iAxpe8q37/rDmf44nyOJGISJPxCi9YsQO+8w9v0gLCFLfH5TzD1V1AYrTAdiF4Y1cgUmP68jP6w==}
+    dependencies:
+      '@vue/reactivity': 3.4.33
+      '@vue/shared': 3.4.33
+
+  /@vue/runtime-dom@3.4.33:
+    resolution: {integrity: sha512-iHsMCUSFJ+4z432Bn9kZzHX+zOXa6+iw36DaVRmKYZpPt9jW9riF32SxNwB124i61kp9+AZtheQ/mKoJLerAaQ==}
+    dependencies:
+      '@vue/reactivity': 3.4.33
+      '@vue/runtime-core': 3.4.33
+      '@vue/shared': 3.4.33
+      csstype: 3.1.3
+
+  /@vue/server-renderer@3.4.33(vue@3.4.33):
+    resolution: {integrity: sha512-jTH0d6gQcaYideFP/k0WdEu8PpRS9MF8d0b6SfZzNi+ap972pZ0TNIeTaESwdOtdY0XPVj54XEJ6K0wXxir4fw==}
+    peerDependencies:
+      vue: 3.4.33
+    dependencies:
+      '@vue/compiler-ssr': 3.4.33
+      '@vue/shared': 3.4.33
+      vue: 3.4.33
+
+  /@vue/shared@3.4.33:
+    resolution: {integrity: sha512-aoRY0jQk3A/cuvdkodTrM4NMfxco8n55eG4H7ML/CRy7OryHfiqvug4xrCBBMbbN+dvXAetDDwZW9DXWWjBntA==}
+
+  /@vueuse/core@9.13.0(vue@3.4.33):
+    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.33)
+      vue-demi: 0.14.9(vue@3.4.33)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+    dev: false
+
+  /@vueuse/metadata@9.13.0:
+    resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
+    dev: false
+
+  /@vueuse/shared@9.13.0(vue@3.4.33):
+    resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
+    dependencies:
+      vue-demi: 0.14.9(vue@3.4.33)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+    dev: false
+
+  /async-validator@4.2.5:
+    resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
+    dev: false
+
+  /asynckit@0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+    dev: false
+
+  /axios@1.7.2:
+    resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==}
+    dependencies:
+      follow-redirects: 1.15.6
+      form-data: 4.0.0
+      proxy-from-env: 1.1.0
+    transitivePeerDependencies:
+      - debug
+    dev: false
+
+  /combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+    dependencies:
+      delayed-stream: 1.0.0
+    dev: false
+
+  /csstype@3.1.3:
+    resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+  /dayjs@1.11.12:
+    resolution: {integrity: sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==}
+    dev: false
+
+  /delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+    dev: false
+
+  /element-plus@2.7.7(vue@3.4.33):
+    resolution: {integrity: sha512-7ucUiDAxevyBE8JbXBTe9ofHhS047VmWMLoksE45zZ08XSnhnyG7WUuk3gmDbAklfVMHedb9sEV3OovPUWt+Sw==}
+    peerDependencies:
+      vue: ^3.2.0
+    dependencies:
+      '@ctrl/tinycolor': 3.6.1
+      '@element-plus/icons-vue': 2.3.1(vue@3.4.33)
+      '@floating-ui/dom': 1.6.8
+      '@popperjs/core': /@sxzz/popperjs-es@2.11.7
+      '@types/lodash': 4.17.7
+      '@types/lodash-es': 4.17.12
+      '@vueuse/core': 9.13.0(vue@3.4.33)
+      async-validator: 4.2.5
+      dayjs: 1.11.12
+      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.33
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+    dev: false
+
+  /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
+    requiresBuild: true
+    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
+    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==}
+
+  /follow-redirects@1.15.6:
+    resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+    dev: false
+
+  /form-data@4.0.0:
+    resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
+    engines: {node: '>= 6'}
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      mime-types: 2.1.35
+    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
+
+  /js-cookie@3.0.5:
+    resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
+    engines: {node: '>=14'}
+    dev: false
+
+  /jsencrypt@3.3.2:
+    resolution: {integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==}
+    dev: false
+
+  /lodash-es@4.17.21:
+    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
+
+  /magic-string@0.30.10:
+    resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==}
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.0
+
+  /memoize-one@6.0.0:
+    resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+    dev: false
+
+  /mime-db@1.52.0:
+    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /mime-types@2.1.35:
+    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+    engines: {node: '>= 0.6'}
+    dependencies:
+      mime-db: 1.52.0
+    dev: false
+
+  /nanoid@3.3.7:
+    resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
+  /normalize-wheel-es@1.2.0:
+    resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
+    dev: false
+
+  /picocolors@1.0.1:
+    resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
+
+  /pinia-plugin-persistedstate@3.2.1(pinia@2.1.7):
+    resolution: {integrity: sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==}
+    peerDependencies:
+      pinia: ^2.0.0
+    dependencies:
+      pinia: 2.1.7(vue@3.4.33)
+    dev: false
+
+  /pinia@2.1.7(vue@3.4.33):
+    resolution: {integrity: sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==}
+    peerDependencies:
+      '@vue/composition-api': ^1.4.0
+      typescript: '>=4.4.4'
+      vue: ^2.6.14 || ^3.3.0
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+      typescript:
+        optional: true
+    dependencies:
+      '@vue/devtools-api': 6.6.3
+      vue: 3.4.33
+      vue-demi: 0.14.9(vue@3.4.33)
+    dev: false
+
+  /postcss@8.4.39:
+    resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==}
+    engines: {node: ^10 || ^12 || >=14}
+    dependencies:
+      nanoid: 3.3.7
+      picocolors: 1.0.1
+      source-map-js: 1.2.0
+
+  /proxy-from-env@1.1.0:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+    dev: false
+
+  /rollup@4.19.0:
+    resolution: {integrity: sha512-5r7EYSQIowHsK4eTZ0Y81qpZuJz+MUuYeqmmYmRMl1nwhdmbiYqt5jwzf6u7wyOzJgYqtCRMtVRKOtHANBz7rA==}
+    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+    hasBin: true
+    dependencies:
+      '@types/estree': 1.0.5
+    optionalDependencies:
+      '@rollup/rollup-android-arm-eabi': 4.19.0
+      '@rollup/rollup-android-arm64': 4.19.0
+      '@rollup/rollup-darwin-arm64': 4.19.0
+      '@rollup/rollup-darwin-x64': 4.19.0
+      '@rollup/rollup-linux-arm-gnueabihf': 4.19.0
+      '@rollup/rollup-linux-arm-musleabihf': 4.19.0
+      '@rollup/rollup-linux-arm64-gnu': 4.19.0
+      '@rollup/rollup-linux-arm64-musl': 4.19.0
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.19.0
+      '@rollup/rollup-linux-riscv64-gnu': 4.19.0
+      '@rollup/rollup-linux-s390x-gnu': 4.19.0
+      '@rollup/rollup-linux-x64-gnu': 4.19.0
+      '@rollup/rollup-linux-x64-musl': 4.19.0
+      '@rollup/rollup-win32-arm64-msvc': 4.19.0
+      '@rollup/rollup-win32-ia32-msvc': 4.19.0
+      '@rollup/rollup-win32-x64-msvc': 4.19.0
+      fsevents: 2.3.3
+    dev: true
+
+  /source-map-js@1.2.0:
+    resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
+    engines: {node: '>=0.10.0'}
+
+  /to-fast-properties@2.0.0:
+    resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
+    engines: {node: '>=4'}
+
+  /vite@5.3.4:
+    resolution: {integrity: sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==}
+    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: '*'
+      stylus: '*'
+      sugarss: '*'
+      terser: ^5.4.0
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+    dependencies:
+      esbuild: 0.21.5
+      postcss: 8.4.39
+      rollup: 4.19.0
+    optionalDependencies:
+      fsevents: 2.3.3
+    dev: true
+
+  /vue-demi@0.14.9(vue@3.4.33):
+    resolution: {integrity: sha512-dC1TJMODGM8lxhP6wLToncaDPPNB3biVxxRDuNCYpuXwi70ou7NsGd97KVTJ2omepGId429JZt8oaZKeXbqxwg==}
+    engines: {node: '>=12'}
+    hasBin: true
+    requiresBuild: true
+    peerDependencies:
+      '@vue/composition-api': ^1.0.0-rc.1
+      vue: ^3.0.0-0 || ^2.6.0
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+    dependencies:
+      vue: 3.4.33
+    dev: false
+
+  /vue-router@4.4.0(vue@3.4.33):
+    resolution: {integrity: sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==}
+    peerDependencies:
+      vue: ^3.2.0
+    dependencies:
+      '@vue/devtools-api': 6.6.3
+      vue: 3.4.33
+    dev: false
+
+  /vue@3.4.33:
+    resolution: {integrity: sha512-VdMCWQOummbhctl4QFMcW6eNtXHsFyDlX60O/tsSQuCcuDOnJ1qPOhhVla65Niece7xq/P2zyZReIO5mP+LGTQ==}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+    dependencies:
+      '@vue/compiler-dom': 3.4.33
+      '@vue/compiler-sfc': 3.4.33
+      '@vue/runtime-dom': 3.4.33
+      '@vue/server-renderer': 3.4.33(vue@3.4.33)
+      '@vue/shared': 3.4.33

BIN
public/favicon.ico


+ 444 - 0
src/App.vue

@@ -0,0 +1,444 @@
+<template>
+
+
+  <el-affix :offset="0" @change="act_trigger">
+    <div class="head" :class="!g_navigationBarTop && 'noHead'">
+      <!-- 移动端更多 -->
+      <div class="head-more" @click="act_updateVisibility(true)">
+        <i class="evenMore">
+          <img src="@/assets/icon/更多.png" alt="">
+        </i>
+      </div>
+
+      <!-- 图标 -->
+      <div class="appIcon">
+        VR_ONE <span></span>
+      </div>
+
+      <!-- 导航栏 -->
+      <div class="navTab">
+        <el-menu :default-active="g_activeName" class="el-menu-demo" mode="horizontal" @select="act_handleSelect"
+          router>
+          <el-menu-item index="/">首页</el-menu-item>
+          <el-menu-item index="/showroomCase">展厅案例</el-menu-item>
+          <el-menu-item index="/templateCase">模板案例</el-menu-item>
+          <el-menu-item index="/ArticleColumn">文章栏目</el-menu-item>
+        </el-menu>
+      </div>
+
+      <!-- 用户功能 -->
+      <div class="userFun">
+        <el-button v-if="!g_isLogin" type="primary" @click="act_Login"
+          style="background-color: rgb(0,0,0);border: none;">登录</el-button>
+
+        <!-- 登录后得状态 -->
+        <div v-if="g_isLogin" class="logging-status">
+          <i class="el-icon-switch-button" @click="act_notificationCenter">
+            <img src="@/assets/icon/信息.png" alt="">
+            </img>
+          </i>
+
+          <!-- 用户下拉 -->
+          <el-dropdown :hide-on-click="false">
+            <span class="el-dropdown-link">
+              <i class="userFun-img"><img src="@/assets/icon/头像.png" alt=""></i>
+            </span>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item @click="act_personalCenter">个人中心</el-dropdown-item>
+                <el-dropdown-item @click="act_myShoppingCart">我的购物车</el-dropdown-item>
+                <el-dropdown-item @click="act_myOrder">我的订单</el-dropdown-item>
+                <el-dropdown-item divided @click="act_logOut">退出</el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </div>
+
+      </div>
+    </div>
+  </el-affix>
+
+
+
+  <!-- 抽屉 -->
+  <drawer v-model="g_drawerVisible" direction="ltr" :size="'70%'">
+
+    <!-- 内容 -->
+    <template v-slot:content>
+      <el-menu :default-active="g_activeName" class="el-menu-vertical-demo" @select="act_handleSelect" router>
+
+        <el-menu-item index="/">
+          <span>首页</span>
+        </el-menu-item>
+
+        <el-menu-item index="/showroomCase">
+          <span>展厅案例</span>
+        </el-menu-item>
+
+        <el-menu-item index="/templateCase">
+          <span>模板案例</span>
+        </el-menu-item>
+
+        <el-menu-item index="/ArticleColumn">
+          <span>文章栏目</span>
+        </el-menu-item>
+
+      </el-menu>
+    </template>
+
+  </drawer>
+
+
+  <!-- 显示区域 -->
+  <RouterView />
+
+  <!-- 尾部 -->
+  <tail v-if="g_navigationBarBottom"></tail>
+
+</template>
+
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import tail from '@/pages/Home/HomeComponents/tail.vue'
+import drawer from '@/components/Common/new-drawer.vue'
+import { ref, onMounted, watchEffect, provide } from 'vue'
+import { useRoute, useRouter, RouterView } from 'vue-router'
+import Cookies from "js-cookie";
+import { encrypt, decrypt } from "@/http/api/general/jsencrypt";
+import { login, getCodeImg } from '@/http/api/pages/User/index'
+import { useUserStore, usePersonalCenterStore } from "@/store/index";
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 获取用户
+const g_userStore = useUserStore();
+// 获取个人中心
+const g_personalCenterStore = usePersonalCenterStore();
+// 获取路由对象
+const g_route = useRoute() // 解构出当前路由对象的各种属性
+const g_router = useRouter() // 解构出路由实例的各种方法
+// 默认选中的页面
+const g_activeName = ref('/')
+// 是否显示底部栏
+const g_navigationBarBottom = ref(true)
+// 导航栏位置
+const g_navigationBarTop = ref(true)
+// 登录状态
+const g_isLogin = ref(false)
+// 抽屉状态
+const g_drawerVisible = ref(false)
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+
+// 页面加载
+onMounted(() => {
+  act_isLogin();
+})
+
+// 监听效果,当效果触发时,会执行提供的函数
+watchEffect(() => {
+  // 将全局变量 g_activeName 的值设置为当前路由路径
+  g_activeName.value = g_route.path
+  if (g_route.path == '/personalCenter') {
+    g_navigationBarBottom.value = false;
+  }
+})
+
+
+
+
+/************************************************
+ * 事件处理
+ *
+ * 格式: act_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 修改抽屉状态
+const act_updateVisibility = (newValue) => {
+  g_drawerVisible.value = newValue
+}
+
+// 菜单激活回调
+const act_handleSelect = (key, keyPath) => {
+  act_updateVisibility(false);
+  g_navigationBarBottom.value = true;
+}
+
+// 登陆注册
+const act_Login = () => {
+  g_router.push("/login")
+}
+
+// 修改导航栏样式
+const act_trigger = () => {
+  g_navigationBarTop.value = !g_navigationBarTop.value
+}
+
+const act_notificationCenter = () => {
+  g_navigationBarBottom.value = false;
+  g_personalCenterStore.setActiveTabName('notificationCenter');
+  g_router.push({ path: "/personalCenter", query: { tab: 'notificationCenter' } })
+}
+
+// 进入个人中心页面
+const act_personalCenter = () => {
+  g_navigationBarBottom.value = false;
+  g_personalCenterStore.setActiveTabName('personalData');
+  g_router.push({ path: "/personalCenter", query: { tab: 'personalData' } })
+}
+
+// 进入我的购物车页面
+const act_myShoppingCart = () => {
+  g_navigationBarBottom.value = false;
+  g_personalCenterStore.setActiveTabName('myShoppingCart');
+  g_router.push({ path: "/personalCenter", query: { tab: 'myShoppingCart' } })
+}
+
+// 进入我的订单页面
+const act_myOrder = () => {
+  g_navigationBarBottom.value = false;
+  g_personalCenterStore.setActiveTabName('myOrder');
+  g_router.push({ path: "/personalCenter", query: { tab: 'myOrder' } })
+}
+
+// 退出登录
+const act_logOut = () => {
+  ElMessageBox.confirm(
+    '确认退出账号吗?',
+    '温馨提示',
+    {
+      confirmButtonText: '确认',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }
+  )
+    .then(() => {
+      g_userStore.clearProfile();
+      g_isLogin.value = false;
+      g_router.push("/login")
+    })
+    .catch(() => {
+
+    })
+
+}
+
+// 判断用户是否登录
+const act_isLogin = () => {
+  try {
+    const loginData = g_userStore.userInfo.user;
+    const token = g_userStore.userInfo.token;
+    // const loginData = Cookies.get("loginData");
+    // const token = Cookies.get("token");
+    if (loginData && token) {
+      g_isLogin.value = true;
+    }
+  } catch (error) {
+    console.error("解析登录数据失败:", error);
+    act_Login();
+  }
+}
+
+// 提供一个函数给子组件调用
+provide('act_isLogin', act_isLogin);
+
+
+</script>
+
+<style scoped>
+.head {
+  padding: 8px 32px 8px calc(2vw + 2vh + 2vmin);
+  background-color: rgb(255, 255, 255, 0.5);
+  backdrop-filter: blur(10px);
+  display: flex;
+  flex-wrap: wrap;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  animation: Element-fall-animation 0.8s ease-in-out;
+}
+
+/* 元素从上下落动画 */
+@keyframes Element-fall-animation {
+  0% {
+    transform: translateY(-100%);
+    opacity: 0;
+  }
+
+  100% {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+.head-more {
+  display: none;
+  cursor: pointer;
+}
+
+.evenMore img {
+  width: 30px;
+  height: 30px;
+}
+
+.appIcon {
+  flex: 1;
+  color: #000000;
+  font-size: 25px;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+}
+
+.appIcon span {
+  width: 20px !important;
+  height: 20px !important;
+  margin-left: 8px;
+  background-color: #FD571F;
+  border-radius: 50%;
+}
+
+.navTab {
+  flex: 10;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.userFun-img {
+  cursor: pointer;
+}
+
+.userFun-img img {
+  width: 28px;
+  height: 28px;
+}
+
+
+.el-menu-demo {
+  background-color: rgb(0, 0, 0, 0);
+}
+
+.box-bottom-text {
+  padding: 32px;
+  color: rgb(255, 255, 255);
+  background-color: #010101;
+  text-align: center;
+}
+
+.el-menu--horizontal {
+  height: 50px;
+}
+
+.demo-tabs>.el-tabs__content {
+  padding: 32px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+
+.el-menu--horizontal>.el-menu-item {
+  color: rgb(0, 0, 0);
+}
+
+.logging-status {
+  display: flex;
+}
+
+.el-icon-switch-button {
+  margin: 0 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.el-icon-switch-button img {
+  width: 24px;
+}
+
+::v-deep .el-menu--horizontal.el-menu {
+  border: none;
+}
+
+.el-image__error {
+  background-color: #1d1d1d;
+}
+
+.el-menu--horizontal.el-menu {
+  flex: 1;
+  border: none;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.el-menu--horizontal>.el-menu-item.is-active {
+  border-bottom: 2px solid #299BFF;
+  color: #299BFF !important;
+}
+
+.el-radio__input.is-checked+.el-radio__label {
+  color: #299BFF;
+}
+
+.el-input__wrapper {
+  background-color: #1d1d1d;
+  box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.3);
+}
+
+.el-menu--horizontal .el-menu-item:not(.is-disabled):focus,
+.el-menu--horizontal .el-menu-item:not(.is-disabled):hover {
+  background-color: rgba(0, 0, 0, 0);
+  color: #299BFF;
+  outline: none;
+}
+
+:focus-visible {
+  outline: -webkit-focus-ring-color auto 0;
+}
+
+/* 隐藏单选框前面点 */
+::v-deep .el-radio__input {
+  display: none;
+}
+
+/* 移动端样式 */
+@media screen and (max-width: 700px) {
+  .el-menu {
+    border-right: none;
+  }
+
+  .head {
+    padding: 16px 8px;
+    border-bottom: 1px solid rgb(214, 214, 214);
+    box-shadow: 0 2px 5px rgb(0, 0, 0, 0.5);
+    overflow: hidden;
+  }
+
+  .head-more {
+    display: flex;
+  }
+
+  .appIcon {
+    justify-content: center;
+  }
+
+  .navTab {
+    display: none;
+  }
+
+  .el-icon-switch-button {
+    display: none;
+  }
+}
+</style>

BIN
src/assets/icon/信息.png


BIN
src/assets/icon/头像.png


BIN
src/assets/icon/更多.png


BIN
src/assets/img/N.png


BIN
src/assets/img/万里茶道.png


BIN
src/assets/img/底部背景.png


BIN
src/assets/img/箭头.png


BIN
src/assets/img/茶道申遗.png


+ 171 - 0
src/components/Common/ImageUpload.vue

@@ -0,0 +1,171 @@
+<template>
+  <el-upload class="avatar-uploader" name="file" method="post" :show-file-list="false" :multiple="false"
+    :auto-upload="false" :on-change="act_handleChange" :file-list="fileList" accept="image/*">
+
+    <el-image v-if="imageUrl" :src="imageUrl" fit="cover" class="avatar" @load="act_handleLoad"
+      @error="act_handleError" />
+
+    <el-icon v-else class="avatar-uploader-icon">
+      <Plus />
+    </el-icon>
+
+  </el-upload>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted, defineProps, watch } from "vue";
+import { Plus } from "@element-plus/icons-vue";
+import axios from "axios";
+
+
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 父组件传递过来的函数
+const g_props = defineProps({
+  handleUploadSuccess: Function,
+  URL: String,
+});
+
+// 上传数据
+const g_uploadData = {
+  directory: "VRONE-IMG",
+};
+
+// 图片地址
+// const imageUrl = ref(g_props.URL ? `http://${g_props.URL}` : "");
+const imageUrl = ref(g_props.URL ? `${g_props.URL}` : "");
+const fileList = ref([]);
+const selectedFile = ref(null);
+// 文件是否改变
+const isFileChanged = ref(false);
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => { });
+
+/************************************************
+ * 事件处理
+ *
+ * 格式: act_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 成功触发
+const act_handleLoad = () => {
+
+}
+
+// 失败触发
+const act_handleError = () => {
+  imageUrl.value = "";
+};
+
+// 选择文件
+const act_handleChange = (file, fileList) => {
+  const reader = new FileReader();
+  reader.onload = (e) => {
+    imageUrl.value = e.target.result;
+  };
+  reader.readAsDataURL(file.raw);
+  selectedFile.value = file.raw;
+  isFileChanged.value = true;
+};
+
+// 保存并上传文件
+const act_uploadFiles = () => {
+  return new Promise((resolve, reject) => {
+    if (!selectedFile.value) {
+      ElMessage.error("请选择文件");
+      resolve();
+      return;
+    }
+
+    const formData = new FormData();
+    formData.append('file', selectedFile.value);
+    formData.append('directory', g_uploadData.directory);
+
+    axios.post('http://106.55.102.172:8010/user/upload', formData, {
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+    })
+      .then(response => {
+        if (response.data.code == 200) {
+          g_props.handleUploadSuccess(response.data.data, "img");
+          resolve();
+        } else {
+          reject(new Error('上传失败'));
+        }
+      })
+      .catch(error => {
+        console.error("上传失败", error);
+        reject(error);
+      });
+  });
+};
+
+// 暴露方法供父组件调用
+defineExpose({
+  act_uploadFiles,
+  isFileChanged
+});
+
+
+/************************************************
+ * 获取数据
+ *
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+
+
+</script>
+
+<style scoped>
+.avatar-uploader {
+  flex: 1;
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.avatar-uploader .el-upload {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+.avatar-uploader .el-upload:hover {
+  border-color: var(--el-color-primary);
+}
+
+.el-icon.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  text-align: center;
+}
+
+::v-deep .el-upload {
+  max-height: 300px;
+  border: 1px solid rgb(204 204 204);
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+::v-deep .avatar {
+  width: 100%;
+}
+</style>

+ 157 - 0
src/components/Common/TablePaging.vue

@@ -0,0 +1,157 @@
+<template>
+    <div class="box">
+        <!-- 表格数据 -->
+        <div class="box-content">
+            <el-table :data="currentTableData" highlight-current-row border table-layout="fixed"
+                style="width: 100%;height: 100%;min-height: 200px" empty-text="暂无数据"
+                @current-change="g_props.currentChange">
+                <slot name="table"></slot>
+            </el-table>
+        </div>
+
+        <!-- 分页栏 -->
+        <div class="box-foot">
+            <el-pagination v-model:current-page="g_PagingData.currentPage" :hide-on-single-page="true"
+                v-model:page-size="g_PagingData.pageSize" :page-sizes="g_PagingData.pageSizes"
+                :small="g_PagingData.small" :disabled="g_PagingData.disabled" :background="g_PagingData.background"
+                layout="total, sizes, prev, pager, next, jumper" :total="g_PagingData.total"
+                @size-change="act_handleSizeChange" @current-change="act_handleCurrentChange" />
+        </div>
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ * 引入文件
+ ************************************************/
+import { ref, reactive, watch, onMounted, defineProps, computed } from 'vue';
+
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 接收外部属性
+const g_props = defineProps({
+    data: Array,
+    currentChange: Function,
+    PagingData: Object,
+});
+
+// 表格数据
+const g_tableData = ref([]);
+
+// 分页数据
+const g_PagingData = reactive({
+    currentPage: 1,
+    pageSize: 10,
+    pageSizes: [10, 20, 50, 200],
+    total: 0,
+    small: false,
+    background: false,
+    disabled: false,
+    ...g_props.PagingData
+});
+
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+
+})
+
+// 计算属性
+const currentTableData = computed(() => {
+    if (!g_tableData.value.length) {
+        return [];
+    }
+    const start = (g_PagingData.currentPage - 1) * g_PagingData.pageSize;
+    const end = start + g_PagingData.pageSize;
+    return g_tableData.value.slice(start, end);
+});
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名 例如: act_handleSizeChange
+************************************************/;
+// 初始化
+const act_init = () => {
+    g_PagingData.total = g_tableData.value.length;
+};
+
+// 改变每页显示条数
+const act_handleSizeChange = (newSize) => {
+    g_PagingData.pageSize = newSize;
+    g_PagingData.currentPage = 1;
+};
+
+// 改变当前页码
+const act_handleCurrentChange = (newPage) => {
+    g_PagingData.currentPage = newPage;
+};
+
+
+// 监听属性变化
+watch(() => g_props.data, (newData) => {
+    if (newData) {
+        g_tableData.value = newData;
+        act_init();
+    }
+}, { immediate: true });
+
+watch(() => g_props.PagingData, (newPagingData) => {
+    if (newPagingData) {
+        Object.assign(g_PagingData, newPagingData);
+    }
+}, { immediate: true });
+</script>
+
+<style scoped>
+.box {
+    width: 100%;
+    height: 100%;
+    min-height: 300px;
+    background-color: rgb(240, 240, 240);
+    border-radius: 8px;
+    display: flex;
+    flex-direction: column;
+}
+
+.box-content {
+    flex: 1 1 1px;
+    overflow: auto;
+}
+
+.box-foot {
+    overflow: auto;
+}
+
+::v-deep .el-pagination {
+    padding: 8px;
+}
+
+/* 自定义滚动条样式 */
+.box-foot::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+}
+
+.box-foot::-webkit-scrollbar-thumb {
+    background-color: rgb(83, 43, 255);
+}
+
+.box-foot::-webkit-scrollbar-thumb:hover {
+    background-color: #50bfff;
+}
+
+.box-foot::-webkit-scrollbar-track {
+    background: #d3d3d3;
+    border: 1px solid rgb(173, 173, 173);
+}
+</style>

+ 175 - 0
src/components/Common/VideoUpload.vue

@@ -0,0 +1,175 @@
+<template>
+  <el-upload class="avatar-uploader" name="file" method="post" :show-file-list="false" :multiple="false"
+    :auto-upload="false" :on-change="act_handleChange" :before-upload="act_beforeUpload" :file-list="fileList"
+    accept="video/*">
+    <!-- 视频播放 -->
+    <video style="height: 300px" v-if="imageUrl" :src="imageUrl" class="avatar" controls @error="act_handleError" />
+
+    <el-icon v-else class="avatar-uploader-icon">
+      <Plus />
+    </el-icon>
+  </el-upload>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted, defineProps, watch } from "vue";
+import { Plus } from "@element-plus/icons-vue";
+import { ElMessage, ElMessageBox } from 'element-plus'
+import axios from "axios";
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 父组件传递过来的函数
+const g_props = defineProps({
+  handleUploadSuccess: Function,
+  URL: String,
+});
+
+// 上传数据
+const g_uploadData = {
+  directory: "VRONE-VIDEO",
+};
+
+// 图片地址
+const imageUrl = ref(g_props.URL ? g_props.URL.indexOf("http") == 0 ? g_props.URL : `http://${g_props.URL}` : "");
+const fileList = ref([]);
+const selectedFile = ref(null);
+// 文件是否改变
+const isFileChanged = ref(false);
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => { });
+
+/************************************************
+ * 事件处理
+ *
+ * 格式: act_ + 事件名     例如: getAppDataList
+ ************************************************/
+
+// 失败触发
+const act_handleError = () => {
+  imageUrl.value = "";
+};
+
+// 选择文件
+const act_handleChange = (file, fileList) => {
+  const reader = new FileReader();
+  reader.onload = (e) => {
+    imageUrl.value = e.target.result;
+  };
+  reader.readAsDataURL(file.raw);
+  selectedFile.value = file.raw;
+  isFileChanged.value = true;
+};
+
+const act_beforeUpload = (file) => {
+  console.debug("act_beforeUpload:", file)
+  const isLt2M = file.size / 1024 / 1024 < 9;
+  if (!isLt2M) {
+    ElMessageBox.alert('上传视频大小不能超过 9MB!', '错误提示');
+  }
+  return isLt2M; // 返回false会阻止上传
+}
+
+// 保存并上传文件
+const act_uploadFiles = () => {
+  return new Promise((resolve, reject) => {
+    if (!selectedFile.value) {
+      ElMessage.error("请选择文件");
+      reject("请选择文件!");
+      return;
+    }
+
+    if (selectedFile.value.size / 1024 / 1024 > 9) {
+      ElMessage.error('上传视频大小不能超过 9MB!');
+      reject('上传视频大小不能超过 9MB!');
+      return;
+    }
+
+    const formData = new FormData();
+    formData.append("file", selectedFile.value);
+    formData.append("directory", g_uploadData.directory);
+
+    axios
+      .post("http://106.55.102.172:8010/user/upload", formData, {
+        headers: {
+          "Content-Type": "multipart/form-data",
+        },
+      })
+      .then((response) => {
+        if (response.data.code == 200) {
+          g_props.handleUploadSuccess(response.data.data, "video");
+          resolve();
+        } else {
+          reject(new Error("上传失败"));
+        }
+      })
+      .catch((error) => {
+        console.error("上传失败", error);
+        reject(error);
+      });
+  });
+};
+
+// 暴露方法供父组件调用
+defineExpose({
+  act_uploadFiles,
+  isFileChanged,
+});
+
+/************************************************
+ * 获取数据
+ *
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+</script>
+
+<style scoped>
+.avatar-uploader {
+  flex: 1;
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.avatar-uploader .el-upload {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+.avatar-uploader .el-upload:hover {
+  border-color: var(--el-color-primary);
+}
+
+.el-icon.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  text-align: center;
+}
+
+::v-deep .el-upload {
+  max-height: 300px;
+  border: 1px solid rgb(204 204 204);
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+::v-deep .avatar {
+  width: 100%;
+}
+</style>

+ 107 - 0
src/components/Common/drawer.vue

@@ -0,0 +1,107 @@
+<!-- 
+    通用组件 : 抽屉 
+
+    
+    父页面使用方式
+    <el-button type="primary" @click="act_updateVisibility(true)">打开抽屉</el-button>
+
+    <drawer :isVisible="g_drawerVisible" @update:isVisible="act_updateVisibility">
+           
+        <template v-slot:title> 标题 </template>
+
+        <template v-slot:content> 内容 </template>
+    </drawer>
+
+    <script setup>
+    // 引入抽屉组件
+    import drawer from '@/components/Common/drawer.vue'
+    // 抽屉状态
+    const g_drawerVisible = ref(false);
+    // 修改抽屉状态
+    const act_updateVisibility = (newValue) => {
+        g_drawerVisible.value = newValue
+    }
+    </script>
+
+-->
+
+
+<template>
+
+    <el-drawer v-model="visible" :title="title" :show-close="true" :size="size" :before-close="CloseCallback"
+        :destroy-on-close="true">
+        <!-- 内容区域 -->
+        <slot name="content">
+
+        </slot>
+    </el-drawer>
+
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, watch, defineEmits, defineProps } from 'vue'
+import { ElButton, ElDrawer } from 'element-plus'
+import { CircleCloseFilled } from '@element-plus/icons-vue'
+
+
+
+/**
+ * 接收的props定义
+ * @property {Boolean} isVisible - 控制组件可见性的布尔值
+ */
+const props = defineProps({
+    isVisible: Boolean,
+    title: String,
+    size: String,
+    CloseCallback: Function,
+})
+
+
+/**
+ * 定义emit函数,用于向父组件发送事件
+ * @property {'update:isVisible'} - 更新可见性的事件名
+ */
+const emit = defineEmits(['update:isVisible'])
+
+
+// 使用ref创建一个响应式变量,初始值为props.isVisible
+const visible = ref(props.isVisible)
+
+
+// 监听props.isVisible的变化,更新visible的值
+watch(() => props.isVisible, (newVal) => {
+    visible.value = newVal
+})
+
+
+// 监听visible的变化,更新props.isVisible的值
+watch(visible, (newVal) => {
+    emit('update:isVisible', newVal)
+})
+
+
+/**
+ * 处理关闭逻辑
+ * 更新visible值为false,并向父组件emit一个更新可见性的事件
+ */
+const handleClose = () => {
+    visible.value = false
+    emit('update:isVisible', false)
+}
+</script>
+
+<style>
+.el-drawer__body {
+    display: flex !important;
+    flex-direction: column !important;
+    flex: 1 !important;
+}
+</style>
+<style scoped>
+.el-icon--left {
+    margin-right: 8px;
+}
+</style>

+ 45 - 0
src/components/Common/new-drawer.vue

@@ -0,0 +1,45 @@
+<!-- 
+    通用组件 : 抽屉 
+-->
+
+
+<template>
+
+    <div class="box">
+        <el-drawer v-bind="$attrs">
+            <!-- 内容区域 -->
+            <slot name="content">
+
+            </slot>
+        </el-drawer>
+    </div>
+
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, watch, useAttrs, defineProps } from 'vue'
+import { ElButton, ElDrawer } from 'element-plus'
+import { CircleCloseFilled } from '@element-plus/icons-vue'
+
+
+</script>
+
+<style>
+.el-drawer__body {
+    display: flex !important;
+    flex-direction: column !important;
+    flex: 1 !important;
+}
+</style>
+<style scoped>
+::v-deep .el-drawer__header {
+    margin-bottom: 0;
+}
+
+.el-icon--left {
+    margin-right: 8px;
+}
+</style>

+ 94 - 0
src/components/Common/slideshow.vue

@@ -0,0 +1,94 @@
+<!-- 轮播图组件 -->
+<template>
+    <div class="box">
+        <!-- 左边内容部分 -->
+        <div class="left-content">
+            <h2>走进未来,一展尽览!</h2>
+            <p>轻松打造专业级3D虚拟显示展厅,<br>
+                让您的品牌与产品绽放新光彩!
+            </p>
+        </div>
+
+        <!-- 右边轮播图部分 -->
+        <div class="right-content">
+            <el-carousel height="420px" motion-blur>
+                <el-carousel-item v-for="item in g_dataList" :key="item.url">
+                    <el-image class="images" :src="item.url" />
+                </el-carousel-item>
+            </el-carousel>
+        </div>
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+
+
+
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+const g_dataList = ref([
+    { url: 'https://img.zcool.cn/community/01643a5d8865c0a801211d5365ddc3.jpg@3000w_1l_2o_100sh.jpg' },
+    { url: 'https://img.zcool.cn/community/013d025ba0c39ca8012099c8889c41.jpg@2o.jpg' },
+    { url: 'https://img.zcool.cn/community/01083a58a024f7a801219c77c370b3.jpg@2o.jpg' },
+    { url: 'https://ts1.cn.mm.bing.net/th/id/R-C.4a81f640a9ab036e948c5eb3e17f9c82?rik=GGe%2fktqAGXB5hQ&riu=http%3a%2f%2fwww.sshuigz.com%2fupload%2fimage%2f20201029%2f20201029015715_33595.jpg&ehk=jAmBfHlGyDNjAgIkFeJO3NvgyhgYD%2bIxMXK6TIRqEzE%3d&risl=&pid=ImgRaw&r=0' },
+    { url: 'https://ts1.cn.mm.bing.net/th/id/R-C.cfc6963a3f9079187d63c467652e8e0e?rik=hzT0XXak%2fXR68Q&riu=http%3a%2f%2fwww.mgtec.net%2fuploadfile%2f2020%2f0429%2f20200429024937205.jpg&ehk=BYsErSkWxgl4jOlMo5Ut7kufPy3Mwr0z8fSBD8EWuYM%3d&risl=&pid=ImgRaw&r=0' },
+])
+</script>
+
+<style scoped>
+/* 最大的盒子 */
+.box {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background-image: url('https://www.imgur.la/images/2024/05/16/2.png');
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    background-color: #1d1d1d;
+}
+
+.left-content {
+    flex: 1;
+    padding: 20px;
+    text-align: center;
+    color: rgb(255, 255, 255);
+}
+
+.right-content {
+    flex: 3;
+    padding: 40px;
+    padding-left: 100px;
+}
+
+.el-carousel__item h3 {
+    color: #475669;
+    opacity: 0.75;
+    line-height: 620px;
+    margin: 0;
+    text-align: center;
+}
+
+.el-carousel__item:nth-child(2n) {
+    background-color: #99a9bf;
+}
+
+.el-carousel__item:nth-child(2n + 1) {
+    background-color: #d3dce6;
+}
+
+/* 图片  */
+.images {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+}
+</style>

+ 30 - 0
src/http/api/general/jsencrypt.js

@@ -0,0 +1,30 @@
+import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
+
+// 密钥对生成 http://web.chacuo.net/netrsakeypair
+
+const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
+  'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
+
+const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
+  '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
+  'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
+  'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
+  'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
+  'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
+  'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
+  'UP8iWi1Qw0Y='
+
+// 加密
+export const encrypt = (txt) => {
+  const encryptor = new JSEncrypt()
+  encryptor.setPublicKey(publicKey) // 设置公钥
+  return encryptor.encrypt(txt) // 对数据进行加密
+}
+
+// 解密
+export const decrypt = (txt) => {
+  const encryptor = new JSEncrypt()
+  encryptor.setPrivateKey(privateKey) // 设置私钥
+  return encryptor.decrypt(txt) // 对数据进行解密
+}
+

+ 11 - 0
src/http/api/general/user.js

@@ -0,0 +1,11 @@
+// 用户相关接口
+
+import serviceAxios from "@/http/index";
+
+// 获取用户信息
+export const getUserData = async () => {
+  return await serviceAxios({
+    url: `/vrone/backend/getInfo`,
+    method: "get",
+  });
+};

+ 15 - 0
src/http/api/pages/Article/index.js

@@ -0,0 +1,15 @@
+import serviceAxios from "@/http/index";
+
+export const getArticleList = () => {
+  return serviceAxios({
+    url: "/vrone/backend/articles/list",
+    method: "get",
+  });
+};
+
+export const getArticle = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/articles/${params}`,
+    method: "get",
+  });
+};

+ 47 - 0
src/http/api/pages/DataSources/index.js

@@ -0,0 +1,47 @@
+// 数据源接口
+
+import serviceAxios from "@/http/index";
+
+// 根据展厅id查询数据源列表
+export const getGalleryDataSources = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/dataSources/getListByAppId/${params}`,
+    method: "get",
+  });
+};
+
+// 新增数据源
+export const addDataSource = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/dataSources",
+    method: "post",
+    data,
+  });
+};
+
+// 修改数据源
+export const updateDataSource = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/dataSources",
+    method: "put",
+    data: data,
+  });
+};
+
+// 卸载数据源
+export const uninstallDataSource = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/spots/unloadSpot",
+    method: "patch",
+    data: data,
+  });
+};
+
+// 挂载数据源
+export const mountDataSource = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/spots/mountSpot",
+    method: "patch",
+    data: data,
+  });
+};

+ 11 - 0
src/http/api/pages/DataType/index.js

@@ -0,0 +1,11 @@
+// 数据源接口
+
+import serviceAxios from "@/http/index";
+
+// 根据展厅id查询数据源列表
+export const getDataType = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/dataTypes/list`,
+    method: "get",
+  });
+};

+ 24 - 0
src/http/api/pages/Favorite/index.js

@@ -0,0 +1,24 @@
+import serviceAxios from "@/http/index";
+
+export const addFavorite = (data) => {
+  return serviceAxios({
+    url: `/vrone/backend/favorites`,
+    method: "post",
+    data,
+  });
+};
+
+export const listFavorite = () => {
+  return serviceAxios({
+    url: "/vrone/backend/favorites/listByUser",
+    method: "get",
+  });
+};
+
+export const removeFavorite = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/favorites",
+    method: "put",
+    data,
+  });
+};

+ 54 - 0
src/http/api/pages/Gallery/index.js

@@ -0,0 +1,54 @@
+// 展厅接口
+
+import serviceAxios from "@/http/index";
+
+// 查询展厅列表
+export const getAppsList = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/apps/list${params}`,
+    method: "get",
+  });
+};
+
+// 新增展厅
+export const addAppsList = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/apps",
+    method: "post",
+    data,
+  });
+};
+
+// 修改展厅
+export const updateAppsList = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/apps",
+    method: "put",
+    data,
+  });
+};
+
+// 根据用户id查询展厅列表
+export const getUserAppsList = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/apps/myAppList/${params}`,
+    method: "get",
+  });
+};
+
+// 获取展厅详细信息
+export const getAppsDetail = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/apps/${params}`,
+    method: "get",
+  });
+};
+
+// 查询用于展示的展厅列表
+export const getAppsShowList = (data = {}) => {
+  return serviceAxios({
+    url: `/vrone/backend/apps/showList`,
+    method: "post",
+    data: data,
+  });
+};

+ 15 - 0
src/http/api/pages/Notice/index.js

@@ -0,0 +1,15 @@
+import serviceAxios from "@/http/index";
+
+export const getNoticeByUserId = () => {
+  return serviceAxios({
+    url: `/vrone/backend/notices/myNoticeByUser`,
+    method: "get",
+  });
+};
+
+export const getNotice = (data) => {
+  return serviceAxios({
+    url: `/vrone/backend/notices/${data}`,
+    method: "get",
+  });
+};

+ 55 - 0
src/http/api/pages/Order/index.js

@@ -0,0 +1,55 @@
+// 场景接口
+
+import serviceAxios from "@/http/index";
+
+// 生成订单
+export const generateOrder = (data) => {
+  return serviceAxios({
+    url: `/vrone/backend/orders/generate`,
+    method: "post",
+    data,
+  });
+};
+
+// 支付订单
+export const payOrder = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/orders/payment",
+    method: "put",
+    data,
+  });
+};
+
+// 取消订单
+export const cancelOrder = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/orders/cancel",
+    method: "put",
+    data,
+  });
+};
+
+// 订单退款
+export const refundOrder = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/orders/refund",
+    method: "put",
+    data,
+  });
+};
+
+// 查询用户订单列表
+export const listUserOrder = () => {
+  return serviceAxios({
+    url: "/vrone/backend/orders/listByUserId",
+    method: "get",
+  });
+};
+
+// 查询订单详情
+export const getOrderDetail = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/orders/" + data,
+    method: "get",
+  });
+};

+ 37 - 0
src/http/api/pages/Scene/index.js

@@ -0,0 +1,37 @@
+// 场景接口
+
+import serviceAxios from "@/http/index";
+
+// 根据用户id查询场景列表
+export const getScenesList = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/scenes/mySceneList/${params}`,
+    method: "get",
+  });
+};
+
+// 新增场景
+export const addScenes = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/scenes",
+    method: "post",
+    data,
+  });
+};
+
+// 获取场景详细信息
+export const getScenesDetail = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/scenes/${params}`,
+    method: "get",
+  });
+};
+
+// 修改场景
+export const updateScenes = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/scenes",
+    method: "put",
+    data,
+  });
+};

+ 37 - 0
src/http/api/pages/Spot/index.js

@@ -0,0 +1,37 @@
+// 场景接口
+
+import serviceAxios from "@/http/index";
+
+// 根据用户场景查询展示点列表
+export const getUserScenepot = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/spots/list/${params}`,
+    method: "get",
+  });
+};
+
+// 根据展厅id查询数据源列表
+export const getDataSourceList = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/dataSources/getListByAppId/${params}`,
+    method: "get",
+  });
+};
+
+// 修改展示点信息
+export const updateSpot = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/spots/editSpot",
+    method: "patch",
+    data: data,
+  });
+};
+
+// 根据场景id和展示点id查询数据源
+export const getDataSourceBySpot = (data) => {
+  return serviceAxios({
+    url: `/vrone/backend/spots/getDataSourceBySpot`,
+    method: "post",
+    data,
+  });
+};

+ 37 - 0
src/http/api/pages/Template/index.js

@@ -0,0 +1,37 @@
+// 模板接口
+
+import serviceAxios from "@/http/index";
+
+// 获取模板列表
+export const getTemplatesList = (params) => {
+  return serviceAxios({
+    url: "/vrone/backend/templates/list",
+    method: "get",
+    params,
+  });
+};
+
+// 查询展厅模板详情
+export const getTemplatesDetail = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/templates/${params}`,
+    method: "get",
+  });
+};
+
+// 查询用户已购买的模板列表
+export const getTemplatesBuyList = (params) => {
+  return serviceAxios({
+    url: `/vrone/backend/templates/myTemplateList/${params}`,
+    method: "get",
+  });
+};
+
+// 查询展示的展厅模板列表
+export const getTemplatesShowList = (data) => {
+  return serviceAxios({
+    url: `/vrone/backend/templates/showList`,
+    method: "post",
+    data,
+  });
+};

+ 76 - 0
src/http/api/pages/User/index.js

@@ -0,0 +1,76 @@
+// 场景接口
+
+import serviceAxios from "@/http/index";
+
+// 获取验证码
+export const getCodeImg = () => {
+  return serviceAxios({
+    url: `/captchaImage`,
+    method: "get",
+  });
+};
+
+// 注册账号
+export const register = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/vrUser/register",
+    method: "post",
+    data,
+  });
+};
+
+// 登录账号
+export const login = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/vrUser/login",
+    method: "post",
+    data: {
+      phone: data.username,
+      password: data.password,
+    },
+  });
+};
+
+// 钱包充值
+export const recharge = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/recharges",
+    method: "post",
+    data,
+  });
+};
+
+// 修改用户邮箱
+export const updateEmail = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/vrUser/editEmil",
+    method: "put",
+    data,
+  });
+};
+
+// 修改用户密码
+export const updatePassword = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/vrUser/editPassword",
+    method: "put",
+    data,
+  });
+};
+
+// 修改用户头像
+export const updateAvatar = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/vrUser/editAvatar",
+    method: "put",
+    data,
+  });
+};
+
+// 获取用户资料
+export const getUserInfo = (data) => {
+  return serviceAxios({
+    url: "/vrone/backend/vrUser/" + data,
+    method: "get",
+  });
+};

+ 8 - 0
src/http/config/index.js

@@ -0,0 +1,8 @@
+const serverConfig = {
+  // baseURL: "http://175.178.124.133:8010", // 请求基础地址,可根据环境自定义
+  baseURL: "http://106.55.102.172:8010", // 请求基础地址,可根据环境自定义
+  resourceBaseUrl: "http://106.55.102.172:10/prod-api", // 资源基础地址
+  useTokenAuthorization: true, // 是否开启 token 认证
+};
+
+export default serverConfig;

+ 84 - 0
src/http/index.js

@@ -0,0 +1,84 @@
+import axios from "axios";
+import serverConfig from "./config";
+import { useUserStore } from "@/store/index";
+import { ElMessageBox } from "element-plus";
+
+// 创建axios请求实例
+const serviceAxios = axios.create({
+  baseURL: serverConfig.baseURL,
+  timeout: 10000,
+  withCredentials: false,
+});
+
+// 创建请求拦截
+serviceAxios.interceptors.request.use(
+  (config) => {
+    config.withCredentials = true;
+
+    // 如果开启 token 认证
+    if (serverConfig.useTokenAuthorization) {
+      const token = useUserStore().userInfo?.token;
+      if (token) {
+        config.headers["Authorization"] = token;
+        config.headers["satoken"] = token;
+      }
+    }
+
+    // 设置请求头
+    if (!config.headers["content-type"]) {
+      config.headers["content-type"] = "application/json";
+      if (config.method === "post") {
+        config.data = JSON.stringify(config.data);
+      }
+    }
+    return config;
+  },
+  (error) => {
+    return Promise.reject(error);
+  }
+);
+
+// 创建响应拦截
+serviceAxios.interceptors.response.use(
+  (res) => {
+    const { data } = res;
+
+    // 处理401未授权情况
+    if (data.code === 401) {
+      showErrorMessage(data.msg);
+      return Promise.reject(data);
+    }
+
+    // 处理500token失效情况
+    if (data.code === 500) {
+      alert(data.msg);
+      useUserStore().clearProfile();
+      window.location.href = "/login";
+      return Promise.reject(data);
+    }
+
+    if (![200, 201].includes(data.code)) {
+      showErrorMessage(data.msg);
+      return Promise.reject(data);
+    }
+
+    return data;
+  },
+  (error) => {
+    // 处理网络错误
+    const errorMessage =
+      error.response?.data?.msg || error.message || "网络请求失败";
+    showErrorMessage(errorMessage);
+    return Promise.reject(error);
+  }
+);
+
+// 显示错误消息
+const showErrorMessage = (message) => {
+  ElMessageBox.alert(message, "提示", {
+    type: "error",
+    confirmButtonText: "确定",
+  });
+};
+
+export default serviceAxios;

+ 26 - 0
src/main.js

@@ -0,0 +1,26 @@
+// 导入必要的模块和库
+import { createApp } from "vue";
+
+// 导入 Pinia 状态管理库
+import pinia from "./store/index";
+
+// 导入 ElementPlus 及其 CSS
+import ElementPlus from "element-plus";
+import "element-plus/dist/index.css";
+
+// 导入 ElementPlus 的中文语言包
+import zhCn from "element-plus/es/locale/lang/zh-cn";
+
+// 导入主应用组件和路由配置
+import App from "./App.vue";
+import router from "./router";
+
+// 创建一个新的 Vue 应用实例
+const app = createApp(App);
+
+app.use(ElementPlus, { locale: zhCn }); // 使用 ElementPlus
+app.use(router); // 使用路由
+app.use(pinia); // 使用 Pinia 状态管理
+
+// 挂载应用到具有 id 'app' 的 HTML 元素上
+app.mount("#app");

+ 183 - 0
src/pages/Article/ArticleColumn.vue

@@ -0,0 +1,183 @@
+<!-- 文章 -->
+
+<template>
+    <div class="box">
+        <h2 class="newstitle">媒体中心</h2>
+        <div class="box-content">
+            <div class="box-content-One news" v-for="item in g_appDataList" :key="item.id" @click="handleClick(item)">
+                <el-container>
+                    <el-aside width="200px" class="image-container">
+                        <img :src="(item.cover?.indexOf('http') == 0 ? '' : 'http://106.55.102.172:10/prod-api') + item.cover"
+                            alt="图片" style="width: 100%;height:100%">
+                    </el-aside>
+                    <el-container style="text-align: left;">
+                        <el-header>
+                            <h3>{{ item.title }}</h3>
+                        </el-header>
+                        <el-main>
+                            <el-text line-clamp="1">
+                                <span v-html="item.content"></span>
+                            </el-text>
+                        </el-main>
+                        <el-footer style="text-align: right;">{{ item.createTime }}</el-footer>
+                    </el-container>
+                </el-container>
+            </div>
+        </div>
+        <div class="mb-4">
+            <el-button round @click="act_loadMore">更多资讯 > ></el-button>
+        </div>
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { getArticleList } from '@/http/api/pages/Article'
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 路由
+const g_route = useRoute();
+const g_router = useRouter();
+
+// 列表数据
+const g_appDataList = ref([])
+
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+    get_articleList();
+})
+
+
+
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+
+const get_articleList = async () => {
+    const res = await getArticleList();
+    g_appDataList.value = res.data;
+}
+
+// 封装到路由
+const handleClick = (item) => {
+    const queryData = {
+        id: item.id,
+        path: "/ArticleColumn",
+        title: "文章栏目"
+    }
+    g_router.push({ path: '/ArticleDetails', query: queryData });
+};
+
+// 更多资讯
+const act_loadMore = () => {
+    console.log("更多资讯")
+}
+
+/************************************************
+ * 获取数据
+ * 
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+
+
+</script>
+
+<style scoped>
+.mb-4 {
+    margin: 16px;
+    text-align: right;
+    padding-right: 26px;
+}
+
+img {
+    width: 100px;
+    height: 80px;
+    border-radius: 16px;
+}
+
+.contentMap {
+    margin: 10px;
+}
+
+.news {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    gap: 16px;
+}
+
+.text {
+    flex: 1;
+    cursor: pointer;
+    transition: all 0.3s ease;
+}
+
+.newstitle {
+    padding-left: 26px;
+}
+
+.title {
+    max-width: 500px;
+    font-weight: bold;
+    font-size: 15px;
+    text-align: left;
+    color: #000;
+}
+
+.box {
+    text-align: center;
+}
+
+.box-content {
+    width: 1200px;
+    text-align: center;
+    margin: 0 auto;
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: center;
+    align-items: center;
+    gap: 16px;
+}
+
+.box-content-One {
+    border: 1px solid #ddd;
+    border-radius: 16px;
+    display: flex;
+    align-items: center;
+    background-color: #f9f9f9;
+    padding: 10px;
+    cursor: pointer;
+    transition: transform 0.2s;
+    /* padding-top: 30px; */
+    width: 90%;
+}
+
+.box-content-One:hover {
+    transform: scale(1.05);
+}
+
+.image-container {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    text-align: center;
+}
+</style>

+ 208 - 0
src/pages/Article/ArtideContent/ArticleDetails.vue

@@ -0,0 +1,208 @@
+<!-- 文章详细页面 -->
+
+<template>
+    <div class="article-container">
+        <!-- 面包屑导航栏 -->
+        <div class="breadcrumb-nav">
+            <el-breadcrumb separator="/">
+                <el-breadcrumb-item :to="{ path: g_breadcrumbsData?.path }">
+                    {{ g_breadcrumbsData?.title }}
+                </el-breadcrumb-item>
+                <el-breadcrumb-item>{{ g_articleData?.title }}</el-breadcrumb-item>
+            </el-breadcrumb>
+        </div>
+
+        <div class="article-wrapper">
+            <!-- 标题 -->
+            <h1 class="article-title">
+                {{ g_articleData?.title }}
+            </h1>
+
+            <!-- 图片 -->
+            <div class="article-banner">
+                <img :src="(g_articleData?.cover?.indexOf('http') == 0 ? '' : 'http://106.55.102.172:10/prod-api') + g_articleData?.cover"
+                    alt="文章配图">
+            </div>
+
+            <!-- 文本内容 -->
+            <article class="article-content" v-html="g_articleData?.content"></article>
+        </div>
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { getArticle } from '@/http/api/pages/Article'
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 路由
+const g_route = useRoute();
+const g_router = useRouter();
+
+// 路由参数
+const g_routeParameter = ref({});
+// 面包屑源头
+const g_breadcrumbsData = ref({ path: "/", title: "首页" });
+// 文章内容
+const g_articleData = ref(null);
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+    act_initRouteQuery();
+    act_BreadcrumbsData();
+    get_articleContent();
+})
+
+
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+// 初始化路由参数
+const act_initRouteQuery = () => {
+    g_routeParameter.value = g_route;
+}
+
+
+
+
+/************************************************
+ * 获取数据
+ * 
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+// 获取文章数据
+const get_articleContent = async () => {
+    // 获取文章内容
+    const res = await getArticle(g_routeParameter.value.query.id);
+    g_articleData.value = res.data;
+}
+
+// 获取来源面包屑
+const act_BreadcrumbsData = () => {
+    g_breadcrumbsData.value = {
+        path: g_routeParameter.value.query.path,
+        title: g_routeParameter.value.query.title
+    }
+}
+</script>
+
+<style scoped>
+.article-container {
+    max-width: 1200px;
+    margin: 0 auto;
+    padding: 20px;
+    background-color: #fff;
+    min-height: 100vh;
+}
+
+.breadcrumb-nav {
+    padding: 16px 0;
+    border-bottom: 1px solid #eee;
+    margin-bottom: 24px;
+}
+
+.article-wrapper {
+    max-width: 860px;
+    margin: 0 auto;
+    padding: 0 20px;
+}
+
+.article-title {
+    font-size: 32px;
+    font-weight: 600;
+    color: #2c3e50;
+    text-align: center;
+    margin: 32px 0;
+    line-height: 1.4;
+}
+
+.article-banner {
+    margin: 32px 0;
+    border-radius: 8px;
+    overflow: hidden;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.article-banner img {
+    width: 100%;
+    height: auto;
+    max-height: 480px;
+    object-fit: cover;
+    display: block;
+}
+
+.article-content {
+    font-size: 16px;
+    line-height: 1.8;
+    color: #2c3e50;
+    margin: 32px 0;
+}
+
+.article-content :deep(h1) {
+    display: none;
+}
+
+.article-content :deep(br) {
+    content: "";
+    display: block;
+    margin: 16px 0;
+}
+
+:deep(.el-breadcrumb) {
+    font-size: 14px;
+}
+
+:deep(.el-breadcrumb__item) {
+    color: #666;
+}
+
+:deep(.el-breadcrumb__inner.is-link) {
+    color: #409EFF;
+    font-weight: normal;
+}
+
+:deep(.el-breadcrumb__inner.is-link:hover) {
+    color: #66b1ff;
+}
+
+@media screen and (max-width: 768px) {
+    .article-container {
+        padding: 16px;
+    }
+
+    .article-wrapper {
+        padding: 0 16px;
+    }
+
+    .article-title {
+        font-size: 24px;
+        margin: 24px 0;
+    }
+
+    .article-banner {
+        margin: 24px -16px;
+        border-radius: 0;
+    }
+
+    .article-content {
+        font-size: 15px;
+        line-height: 1.7;
+    }
+}
+</style>

ファイルの差分が大きいため隠しています
+ 349 - 0
src/pages/Gallery/showroomCase.vue


ファイルの差分が大きいため隠しています
+ 287 - 0
src/pages/Gallery/showroomCaseDetail.vue


+ 181 - 0
src/pages/Home/HomeComponents/basic_introduction.vue

@@ -0,0 +1,181 @@
+<!-- 基础介绍 -->
+
+<template>
+    <div class="box">
+
+        <div class="box-content">
+
+            <div v-for="item in g_dataList" :key="item.id" class="box-item">
+                <div class="box-item-ico">
+                    <img :src="item.bgUrl" alt="">
+                </div>
+                <!-- 中文标题 -->
+                <div class="box-item-title"> {{ item.title }}</div>
+                <!-- 内容 -->
+                <div class="box-item-content"> {{ item.content }}</div>
+                <!-- 英文标题 -->
+                <div class="box-item-englishTitle">{{ item.englishTitle }}</div>
+            </div>
+
+        </div>
+
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref } from "vue";
+
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+const g_dataList = ref([
+    {
+        id: 1,
+        bgUrl: "https://img.js.design/assets/img/66a9dd7306bb4bea863aaa61.png",
+        title: "虚拟现实创意探索",
+        content: "沉浸在虚拟现实的世界,开启一场前所未有的创意之旅!在这里,每一步都是一次新的发现,每一瞥都是一个创意的火花。让我们带你穿越现实与虚拟的边界,体验未来科技与艺术的完美融合。",
+        englishTitle: "VR Creative Exploration"
+    },
+    {
+        id: 2,
+        bgUrl: "https://img.js.design/assets/img/657fb580c682b8ef43fba7e3.png",
+        title: "穿越虚实,开启创意之旅",
+        content: "穿越现实与虚拟的边界,开启无尽的创意之旅!在我们的VR展馆中,感受创意的无限可能,体验科技与艺术的交汇之美。每一个展区都将带给你全新的灵感与创意,让你沉浸在未来的奇幻世界中。",
+        englishTitle: "Cross Realities, Start Creating"
+    },
+    {
+        id: 3,
+        bgUrl: "https://img.js.design/assets/img/65832f5a50dcc71b5afea260.png",
+        title: "戴上VR,探索创意无限!",
+        content: "进入虚拟现实的奇幻世界,开启一场前所未有的创意冒险!在VR展馆中,感受未来的艺术与科技,体验创意的极致魅力。让虚拟现实为你打开创意的大门,每一步都是灵感的源泉。",
+        englishTitle: "VR On, Explore Creativity!"
+    },
+    {
+        id: 4,
+        bgUrl: "https://img.js.design/assets/img/657d6aa74725d8554c152408.png",
+        title: "沉浸式体验",
+        content: "VR展厅通过虚拟现实技术,为观众呈现一个三维的虚拟环境。观众可以自由地与展品进行互动,获得身临其境的感受。这种沉浸式的体验能够让观众更加深入地了解展示内容,提高参与度和体验感。",
+        englishTitle: "Immersive Experience"
+    },
+]);
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+
+
+
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+
+
+
+
+/************************************************
+ * 获取数据
+ * 
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+}
+
+.box-content {
+    padding: 0 calc(2vw + 2vh + 2vmin) calc(1.5vw + 1.5vh + 1.5vmin) calc(2vw + 2vh + 2vmin);
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
+    justify-content: space-between;
+    align-items: flex-start;
+    grid-gap: 32px;
+}
+
+.box-item {
+    width: 100%;
+    cursor: pointer;
+    animation: Fade-animation 0.8s ease-in-out;
+}
+
+/* 元素渐显动画 */
+@keyframes Fade-animation {
+    0% {
+        opacity: 0;
+        transform: scale(0.8);
+    }
+
+    100% {
+        opacity: 1;
+        transform: scale(1);
+    }
+}
+
+.box-item:hover .box-item-ico {
+    transform: scale(1.2);
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.box-item-ico {
+    width: 100%;
+    height: 100%;
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.box-item-ico img {
+    width: 30%;
+    height: 30%;
+}
+
+.box-item:hover .box-item-title {
+    transform: scale(1.2);
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.box-item-title {
+    font-size: 100%;
+    font-weight: 600;
+    color: rgba(0, 0, 0, 1);
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.box-item:hover .box-item-content {
+    transform: scale(1.1);
+    color: rgb(0, 0, 0);
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.box-item-content {
+    padding: 16px 0;
+    line-height: 22px;
+    font-size: 14px;
+    font-weight: 400;
+    color: rgba(148, 148, 148, 1);
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.box-item:hover .box-item-englishTitle {
+    transform: scale(1.1);
+    color: rgb(78, 78, 78);
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.box-item-englishTitle {
+    font-size: 14px;
+    font-weight: 400;
+    color: rgba(148, 148, 148, 1);
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+</style>

+ 272 - 0
src/pages/Home/HomeComponents/head.vue

@@ -0,0 +1,272 @@
+<!-- 头部 -->
+
+<template>
+
+    <div class="box">
+
+        <div class="box-content">
+            <!-- 左边框 -->
+            <div class="box-content-left">
+                <p class="box-content-left-title">走进未来,一展尽览!</p>
+                <div class="box-content-left-text">
+                    <p>轻松打造专业级3D虚拟显示展厅</p>
+                    <p>让您的品牌与产品绽放新光彩!</p>
+                </div>
+                <div class="box-content-left-btn">
+                    <el-button type="primary" class="btn-go" @click="act_Login">开始使用</el-button>
+                    <el-button type="primary" plain class="btn-design">在线资询</el-button>
+                </div>
+
+                <div class="box-content-left-bg">
+                    <div class="box-content-left-bg-item"></div>
+                    <div class="box-content-left-bg-item"></div>
+                    <div class="box-content-left-bg-item"></div>
+                </div>
+            </div>
+
+            <!-- 右边框 -->
+            <div class="box-content-right">
+                <div class="box-content-right-bg">
+                    <div class="box-content-right-bg-item"></div>
+                    <div class="box-content-right-bg-item"></div>
+                    <div class="box-content-right-bg-item"></div>
+                </div>
+            </div>
+        </div>
+
+    </div>
+
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+
+import { useRoute, useRouter, RouterView } from 'vue-router'
+
+import { useUserStore } from "@/store/index";
+
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 获取路由对象
+const g_route = useRoute() // 解构出当前路由对象的各种属性
+const g_router = useRouter() // 解构出路由实例的各种方法
+const g_userStore = useUserStore();
+
+
+
+/************************************************
+ * 事件处理
+ *
+ * 格式: act_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 登陆注册
+const act_Login = () => {
+    if (g_userStore.userInfo.user && g_userStore.userInfo.token) {
+        g_router.push("/personalCenter");
+    } else {
+        g_router.push("/login");
+    }
+}
+
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    display: flex;
+}
+
+.box-content {
+    flex: 1;
+    min-height: calc(15vw + 20vh + 10vmin);
+    height: 100%;
+    /* display: flex; */
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
+}
+
+.box-content-left {
+    position: relative;
+    flex: 2;
+    padding: calc(1.5vw + 1.5vh + 1.5vmin) calc(2vw + 2vh + 2vmin);
+    overflow: hidden;
+}
+
+.box-content-left-title {
+    font-size: calc(1.2vw + 1.4vh + 14px);
+    font-weight: 600;
+    letter-spacing: 0px;
+    line-height: 43.2px;
+    color: rgba(0, 0, 0, 1);
+    animation: wordDrop 0.8s ease-in-out;
+}
+
+/* 文字向下滑落动画 */
+@keyframes wordDrop {
+    0% {
+        transform: translateY(-100%);
+        opacity: 0;
+    }
+
+    100% {
+        transform: translateY(0);
+        opacity: 1;
+    }
+}
+
+/* 文字从左侧滑出动画 */
+@keyframes wordSlide {
+    0% {
+        transform: translateX(-100%);
+        opacity: 0;
+    }
+
+    100% {
+        transform: translateX(0);
+        opacity: 1;
+    }
+}
+
+.box-content-left-text {
+    font-size: calc(0.5vw + 0.5vh + 10px);
+    font-weight: 400;
+    color: rgba(148, 148, 148, 1);
+    animation: wordSlide 0.8s ease-in-out;
+}
+
+.box-content-left-btn {
+    margin-top: 32px;
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+}
+
+.box-content-left-btn button {
+    min-width: 120px;
+    width: calc(5vw + 5vh + 5vmin);
+    min-height: 50px;
+    height: calc(1vw + 1vh + 2vmin);
+    border-radius: 2px;
+}
+
+.box-content-left-btn>.btn-go {
+    background: rgba(41, 155, 255, 1);
+}
+
+.box-content-left-bg-item:nth-child(1) {
+    position: absolute;
+    left: 10%;
+    top: 101px;
+    width: 15vw;
+    height: 15vw;
+    background: linear-gradient(125.99deg, rgba(179, 223, 255, 1) 0%, rgb(123, 119, 237, 0.5) 100%);
+    border: 1px solid rgba(0, 0, 0, 1);
+    box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25);
+    border-radius: 50%;
+    filter: blur(50px);
+    z-index: -1;
+}
+
+.box-content-left-bg-item:nth-child(2) {
+    position: absolute;
+    left: 5%;
+    top: 282px;
+    width: 8vw;
+    height: 8vw;
+    background: linear-gradient(125.99deg, rgba(130, 203, 255, 1) 0%, rgba(35, 255, 31, 0.5) 100%);
+    border: 1px solid rgba(0, 0, 0, 1);
+    box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25);
+    border-radius: 50%;
+    filter: blur(50px);
+    z-index: -1;
+}
+
+.box-content-left-bg-item:nth-child(3) {
+    position: absolute;
+    left: -10%;
+    top: 450px;
+    width: 8vw;
+    height: 8vw;
+    background: linear-gradient(125.99deg, rgba(130, 203, 255, 1) 0%, rgba(225, 191, 255, 0.01) 100%);
+    border: 1px solid rgba(0, 0, 0, 1);
+    box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25);
+    border-radius: 50%;
+    filter: blur(50px);
+    z-index: -1;
+}
+
+.box-content-right {
+    position: relative;
+    flex: 3;
+}
+
+.box-content-right-bg-item:nth-child(1) {
+    position: absolute;
+    right: 75%;
+    top: 122px;
+    width: 20vw;
+    height: 20vw;
+    background: linear-gradient(145.61deg, rgba(255, 255, 255, 1) 0%, rgba(201, 212, 244, 1) 100%);
+    border-radius: 50%;
+    filter: blur(4px);
+    z-index: -1;
+}
+
+.box-content-right-bg-item:nth-child(2) {
+    position: absolute;
+    right: 50%;
+    top: 112px;
+    width: 8vw;
+    height: 8vw;
+    background: linear-gradient(145.61deg, rgba(255, 255, 255, 1) 0%, rgba(255, 207, 246, 1) 100%);
+    border-radius: 50%;
+    filter: blur(4px);
+    z-index: -1;
+}
+
+.box-content-right-bg-item:nth-child(3) {
+    position: absolute;
+    right: 10%;
+    top: 318px;
+    width: 12vw;
+    height: 12vw;
+    border-radius: 50%;
+    background: linear-gradient(145.61deg, rgba(255, 255, 255, 1) 0%, rgba(178, 216, 227, 1) 100%);
+    filter: blur(4px);
+    z-index: -1;
+}
+
+/* 移动端样式 */
+@media screen and (max-width: 700px) {
+
+    .box-content-right-bg-item:nth-child(1) {
+        right: 75%;
+        top: 0;
+        width: 100px;
+        height: 100px;
+        transform: translate(0, -200%);
+    }
+
+    .box-content-right-bg-item:nth-child(2) {
+        right: 50%;
+        top: 0;
+        width: 40px;
+        height: 40px;
+        transform: translate(50%, -250%);
+    }
+
+    .box-content-right-bg-item:nth-child(3) {
+        right: 10%;
+        top: 0;
+        width: 60px;
+        height: 60px;
+        transform: translate(50%, -250%);
+    }
+}
+</style>

+ 302 - 0
src/pages/Home/HomeComponents/news_information.vue

@@ -0,0 +1,302 @@
+<!-- 新闻资讯 -->
+
+<template>
+    <div class="box">
+
+        <div class="box-content">
+
+            <!-- 左边框 -->
+            <div class="box-content-left">
+                <p>Network</p>
+                <p>Information</p>
+                <p>新闻资讯</p>
+            </div>
+
+            <!-- 右边框 -->
+            <div class="box-content-right">
+
+                <div v-for="item in g_dataList" :key="item.id" class="box-content-right-item">
+                    <!-- 图片 -->
+                    <div class="box-content-item-img">
+                        <img :src="(item.cover?.indexOf('http') == 0 ? '' : 'http://106.55.102.172:10/prod-api') + item.cover"
+                            alt="图片">
+                    </div>
+                    <!-- 标题 -->
+                    <div class="box-content-item-title">
+                        <span>{{ item.title }}</span>
+                        <span>{{ item.createTime }}</span>
+                    </div>
+                    <!-- 内容 -->
+                    <div class="box-content-item-content">
+                        <span v-html="item.content"></span>
+                    </div>
+                    <!-- 按钮 -->
+                    <div class="box-content-item-btn">
+                        <el-button type="primary" @click="handleClick(item)">了解更多</el-button>
+                    </div>
+                </div>
+
+            </div>
+
+        </div>
+
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { getArticleList } from '@/http/api/pages/Article'
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 路由
+const g_route = useRoute();
+const g_router = useRouter();
+
+
+const g_dataList = ref([
+    {
+        id: 1,
+        bgUrl: "https://th.bing.com/th/id/R.3ca039d0cbb53db344864ed1ccf0ca46?rik=p374KM4KMfilRA&riu=http%3a%2f%2fwww.cq.gov.cn%2fywdt%2ftpxw%2f202106%2fW020210603363738010006.jpg&ehk=FNntSzzIDfjvBr3SvvcgESWvbPQChRm1iZa58ZhPBaw%3d&risl=&pid=ImgRaw&r=0",
+        title: "傅柒生:万里茶道申遗重在联合,也要突出特色",
+        date: "2023-8-3",
+        content: "11月20日至22日,中蒙俄万里茶道申遗国际学术研讨会暨万里茶道沿线城市市长论坛在黄山举行,会议期间,福建省文化和旅游厅党组成员、副厅长、福建省文物局局长傅柒生在现场接受了荆楚网记者采访。",
+        englishTitle: "VR Creative Exploration",
+    },
+    {
+        id: 2,
+        bgUrl: "https://media.istockphoto.com/id/2058934246/zh/%E7%85%A7%E7%89%87/spring-field-drone-photography-sustainability-aerial-landscape-green-economy.jpg?s=1024x1024&w=is&k=20&c=kNtLb-ji23MbGcOOTLyNWtUG-TcpwfCod9dpQl5lbQs=",
+        title: "蒙古国文化部代表就万里茶道联合跨国申遗工作进行展望",
+        date: "2023-8-3",
+        content: "11月20日至22日,中蒙俄万里茶道申遗国际学术研讨会暨万里茶道沿线城市市长论坛成功在黄山举行,蒙古国文化部文物局文化遗产及考古司主任阿拉达尔孟克呼钦(ALDARMUNKH Purev)在现场接受了记者采访,并对万里茶道联合跨国申遗工作进行展望。",
+        englishTitle: "VR Creative Exploration"
+    },
+    {
+        id: 3,
+        bgUrl: "https://img.zcool.cn/community/014cc05d2ec104a80120695c86c6f5.jpg@1280w_1l_2o_100sh.jpg",
+        title: "傅柒生:万里茶道申遗重在联合,也要突出特色",
+        date: "2023-8-3",
+        content: "11月20日至22日,中蒙俄万里茶道申遗国际学术研讨会暨万里茶道沿线城市市长论坛成功在黄山举行。太原市政协副主席陈晓红,太原市文物局党组书记、局长刘玉伟接受了记者采访。",
+        englishTitle: "VR Creative Exploration"
+    },
+]);
+
+
+onMounted(() => {
+    get_articleList();
+})
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+
+
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+// 
+const handleClick = (item) => {
+    const queryData = {
+        id: item.id,
+        path: "/ArticleColumn",
+        title: "文章栏目"
+    }
+    g_router.push({ path: '/ArticleDetails', query: queryData });
+}
+
+const get_articleList = async () => {
+    const res = await getArticleList();
+    g_dataList.value = res.data.filter((item, index) => index < 3);
+}
+
+/************************************************
+ * 获取数据
+ * 
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    display: flex;
+}
+
+.box-content {
+    flex: 1;
+    min-height: calc(15vw + 20vh + 10vmin);
+    height: 100%;
+    display: flex;
+    flex-wrap: wrap;
+}
+
+.box-content-left {
+    flex: 1;
+    padding: calc(1.5vw + 1.5vh + 1.5vmin) calc(2vw + 2vh + 2vmin);
+}
+
+.box-content-left p:nth-child(1),
+.box-content-left p:nth-child(2) {
+    font-size: 24px;
+    font-weight: 600;
+    color: rgba(0, 0, 0, 1);
+    white-space: nowrap;
+}
+
+.box-content-left p:nth-child(3) {
+    font-size: 20px;
+    font-weight: 400;
+    color: rgba(0, 0, 0, 1);
+}
+
+
+.box-content-right {
+    flex: 11;
+    margin: 100px 0;
+    padding: 0 40px 0 70px;
+    background-color: rgba(235, 235, 235, 1);
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
+    grid-gap: 80px;
+}
+
+.box-content-right-item {
+    position: relative;
+    min-height: calc(15vw + 10vh + 10vmin);
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+}
+
+.box-content-item-img {
+    position: relative;
+    top: -50px;
+    width: calc(100% - 32px);
+    height: calc(2vw + 2vh + 10vmin);
+    background-size: cover;
+    background-position: center;
+    z-index: 1;
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.box-content-item-img img {
+    width: 100%;
+    height: 100%;
+}
+
+.box-content-item-img::after {
+    content: '';
+    position: absolute;
+    top: calc(0px - 0.1vw - 0.1vh - 0.1vmin);
+    right: 0;
+    width: 50%;
+    height: calc(0.1vw + 0.1vh + 0.1vmin);
+    background-color: #FD571F;
+}
+
+.box-content-item-img::before {
+    content: '';
+    position: absolute;
+    top: calc(0px - 0.1vw - 0.1vh - 0.1vmin);
+    right: calc(0px - 0.2vw - 0.1vh - 0.1vmin);
+    width: calc(0.2vw + 0.1vh + 0.1vmin);
+    height: 70%;
+    background-color: #FD571F;
+}
+
+.box-content-item-title {
+    position: relative;
+    top: -25px;
+    display: flex;
+    justify-content: space-between;
+    white-space: nowrap;
+}
+
+.box-content-item-title span:nth-child(1) {
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.box-content-item-title span:nth-child(2) {
+    margin: 0 0 0 32px;
+}
+
+
+.box-content-item-content {
+    position: relative;
+    padding-top: 16px;
+    margin: 0 0 32px 0;
+    font-size: calc(0.2vw + 0.2vh + 10px);
+    font-weight: 400;
+    border-top: 1px solid #D9D9D9;
+    text-align: justify;
+    color: rgba(77, 77, 77, 1);
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 5;
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+
+.box-content-item-btn button {
+    min-width: 100px;
+    width: calc(2vw + 2vh + 10vmin);
+    min-height: 40px;
+    height: calc(1.5vw + 1vh + 1vmin);
+    background-color: rgba(0, 0, 0, 1);
+    border: none;
+    border-radius: 0;
+    font-size: calc(0.2vw + 0.2vh + 10px);
+}
+
+
+/* 移动端样式 */
+@media screen and (max-width: 700px) {
+
+    .box-content-left {
+        padding: calc(1.8vw + 1.8vh + 1.8vmin) calc(2vw + 2vh + 2vmin);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        flex-direction: column;
+    }
+
+    .box-content-left p:nth-child(1),
+    .box-content-left p:nth-child(2),
+    .box-content-left p:nth-child(3) {
+        margin: 0 8px;
+    }
+
+    .box-content-right {
+        flex: 11;
+        margin: 0;
+        padding: 60px calc(2vw + 2vh + 2vmin);
+        background-color: rgb(255, 255, 255);
+        display: grid;
+        grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
+    }
+
+    .box-content-item-img[data-v-c233680a] {
+        position: relative;
+        top: -50px;
+        width: calc(100% - 32px);
+        height: calc(2vw + 2vh + 25vmin);
+        background-size: cover;
+        background-position: center;
+        z-index: 1;
+        transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+    }
+}
+</style>

+ 346 - 0
src/pages/Home/HomeComponents/recent_project.vue

@@ -0,0 +1,346 @@
+<!-- 近期项目 -->
+
+<template>
+
+    <div class="box">
+        <div class="box-content">
+
+            <div class="box-content-top">
+                <div class="box-top-englishTitle">
+                    <p>LATEST</p>
+                    <p>PROJECTS</p>
+                </div>
+
+                <div class="box-top-title">
+                    <span>•</span>
+                    <span>近期项目</span>
+                </div>
+            </div>
+
+            <div class="box-content-bottom">
+                <!-- 万里茶道申遗图片 -->
+                <div class="box-content-bottom-item"></div>
+
+                <!-- 内容 -->
+                <div class="box-content-bottom-item">
+                    <div class="box-content-bottom-item-left">
+                        <div class="item-text-title">
+                            <p>万里茶道申遗</p>
+                            <p>Ten Thousand Miles Tea Ceremony World Heritage Application</p>
+                        </div>
+
+                        <div class="item-text-content">
+                            <p>
+                                万里茶道是古代中国、蒙古、俄国之间以茶叶为大宗商品的长距离贸易线路,是继丝绸之路衰落之后在欧亚大陆兴起的又一条重要的国际商道。
+                            </p>
+                            <p>
+                                万里茶道从中国福建崇安(现武夷山市)起,途经江西、湖南、湖北、河南、山西、河北、内蒙古、从伊林(现二连浩特)进入现蒙古国境内、沿阿尔泰军台,穿越沙漠戈壁,经库伦(现乌兰巴托)到达中俄边境的通商口岸恰克图。
+                            </p>
+                            <p>
+                                全程约4760公里,其中水路1480公里,陆路3280公里。
+                            </p>
+                            <p>
+                                茶道在俄罗斯境内继续延伸,从恰克图经伊尔库茨克、新西伯利亚、秋明、莫斯科、圣彼得堡等十几个城市,又传入中亚和欧洲其他国家,使茶叶之路干线总长13000
+                                余公里, [16]沟通了亚洲大陆南北方向农耕文明与草原游牧文明的核心区域,并延伸至中亚和东欧等地区。成为名副其实的“万里茶路”。
+                            </p>
+                            <p>
+                                2019年3月22日,国家文物局发函,正式同意将“万里茶道”列入《中国世界文化遗产预备名单》。
+                            </p>
+                            <p>
+                                2024年4月18日,第九届中蒙俄万里茶道城市合作大会在湖北孝感汉川开幕。
+                            </p>
+                        </div>
+
+                        <div class="item-text-related">
+                            <div>
+                                <p>申遗</p>
+                                <p>Inscription</p>
+                            </div>
+                            <div>
+                                <p>茶</p>
+                                <p>Tea</p>
+                            </div>
+                            <div>
+                                <p>展厅</p>
+                                <p>Gallery</p>
+                            </div>
+                            <div>
+                                <p>虚拟现实</p>
+                                <p>Virtual Reality</p>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="box-content-bottom-item-right">
+                        <!-- 跳转 -->
+                        <div class="bottom-item-jumpBackground" @click="act_toTeaCeremony">
+                            <span class="bottom-item-jumpBackground-text">SKIP</span>
+                            <img class="bottom-item-jumpBackground-img" src="@/assets/img/箭头.png" alt="">
+                        </div>
+                    </div>
+                </div>
+
+            </div>
+
+            <div class="bottom-item-jumpBackground noPC" @click="act_toTeaCeremony">
+                <span class="bottom-item-jumpBackground-text">SKIP</span>
+                <img class="bottom-item-jumpBackground-img" src="@/assets/img/箭头.png" alt="">
+            </div>
+
+        </div>
+    </div>
+
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+
+
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+
+
+
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+// 跳转页面
+const act_toTeaCeremony = () => {
+    window.open("http://www.greattearoute.com/index.php/index/index/index", "Inscription")
+}
+
+
+
+
+
+/************************************************
+ * 获取数据
+ * 
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    display: flex;
+}
+
+.box-content {
+    flex: 1;
+    padding: calc(1.8vw + 1.8vh + 1.8vmin) calc(2vw + 2vh + 2vmin);
+    display: flex;
+    flex-direction: column;
+}
+
+.box-content-top {
+    flex: 1;
+    display: flex;
+    justify-content: space-between;
+    padding-bottom: 8px;
+    border-bottom: 1px solid rgba(204, 204, 204, 1);
+}
+
+.box-top-englishTitle {
+    font-size: 24px;
+    font-weight: 600;
+    color: rgba(0, 0, 0, 1);
+    text-align: left;
+    vertical-align: top;
+}
+
+.box-top-englishTitle p:nth-child(1) {
+    margin: 0px;
+}
+
+.box-top-englishTitle p:nth-child(2) {
+    margin: 0px calc(0.5vw + 0.5vh + 0.5vmin);
+}
+
+.box-top-title {
+    font-size: 24px;
+    font-weight: 400;
+    color: rgba(0, 0, 0, 1);
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+}
+
+.box-top-title span:nth-child(1) {
+    color: rgba(253, 87, 31, 1);
+    font-size: 40px;
+}
+
+.box-content-bottom {
+    margin-top: 40px;
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
+    justify-content: space-between;
+    align-items: flex-start;
+    grid-gap: 32px;
+}
+
+.box-content-bottom .box-content-bottom-item:nth-child(1) {
+    width: 100%;
+    height: calc(10vw + 10vh + 10vmin);
+    background-image: url("@/assets/img/茶道申遗.png");
+    background-size: cover;
+    background-position: center;
+    background-repeat: no-repeat;
+    box-shadow: 8px 8px 8px rgb(0, 0, 0, 0.3);
+}
+
+.box-content-bottom .box-content-bottom-item:nth-child(2) {
+    min-height: calc(8vw + 8vh + 5vmin);
+    /* padding: 32px 64px 0 64px; */
+    display: flex;
+    flex-direction: row;
+}
+
+.box-content-bottom-item-left {
+    flex: 3;
+}
+
+.box-content-bottom-item-right {
+    flex: 1;
+    display: flex;
+    justify-content: center;
+}
+
+.item-text-title p:nth-child(1) {
+    margin: 0;
+    font-size: 24px;
+    color: rgba(0, 0, 0, 1);
+}
+
+.item-text-title p:nth-child(2) {
+    margin: 2px 0 32px 0;
+    font-size: 16px;
+    color: rgba(0, 0, 0, 1);
+}
+
+.item-text-content {
+    font-size: 16px;
+    color: rgba(148, 148, 148, 1);
+}
+
+.item-text-related {
+    font-size: 12px;
+    color: rgba(71, 71, 71, 1);
+    display: flex;
+    justify-content: flex-start;
+}
+
+.item-text-related div {
+    margin-right: 32px;
+}
+
+.bottom-item-jumpBackground {
+    position: relative;
+    width: 125px;
+    height: 125px;
+    border-radius: 50%;
+    color: rgb(255, 255, 255);
+    background-color: rgb(0, 0, 0);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: column;
+    cursor: pointer;
+    overflow: hidden;
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.bottom-item-jumpBackground:hover {
+    background-color: brown;
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.noPC {
+    display: none;
+}
+
+.bottom-item-jumpBackground-img {
+    position: absolute;
+    top: 60%;
+    left: -100%;
+    min-width: 35px;
+    width: calc(1vw + 1vh + 1vmin);
+    opacity: 1;
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.bottom-item-jumpBackground-text {
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.bottom-item-jumpBackground:hover .bottom-item-jumpBackground-text {
+    color: rgba(253, 87, 31, 1);
+    transform: scale(1.1);
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.bottom-item-jumpBackground:hover .bottom-item-jumpBackground-img {
+    left: 50%;
+    transform: translateX(-50%);
+    opacity: 1;
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+/* 移动端样式 */
+@media screen and (max-width: 700px) {
+    .box-content-bottom {
+        flex-direction: column;
+    }
+
+    .bottom-item-jumpBackground:nth-child(1) {
+        display: none;
+    }
+
+    .box-content-bottom-item-right {
+        display: none;
+    }
+
+    .noPC {
+        position: relative;
+        width: 100%;
+        height: 70px;
+        border-radius: 0;
+        background: rgba(235, 235, 235, 1);
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        flex-direction: column;
+        cursor: pointer;
+        overflow: hidden;
+    }
+
+    .bottom-item-jumpBackground-text {
+        color: rgba(253, 87, 31, 1);
+    }
+
+    .bottom-item-jumpBackground-img {
+        top: 65%;
+        left: 50%;
+        transform: translateX(-50%);
+    }
+}
+</style>

+ 192 - 0
src/pages/Home/HomeComponents/tail.vue

@@ -0,0 +1,192 @@
+<!-- 新闻资讯 -->
+
+<template>
+
+    <div class="box">
+
+        <div class="box-content">
+
+            <div class="box-top">
+                <!-- 左边框 -->
+                <div class="box-content-left">
+                    <p>SITE BOTTOM </p>
+                    <p>BAR<i class="box-content-left-line"></i></p>
+                </div>
+
+                <!-- 右边框 -->
+                <div class="box-content-right">
+                    <ul class="box-content-right-item">
+                        <li class="ul-title">NAVIGATION | 导航</li>
+                    </ul>
+                    <ul class="box-content-right-item">
+                        <li class="ul-title">PRODUCT | 产品</li>
+                        <li class="ul-item">购买展厅模板</li>
+                        <li class="ul-item">协议管理平台</li>
+                        <li class="ul-item">团队项目管理平台</li>
+                        <li class="ul-item">API FOX - 接口文档神器</li>
+                    </ul>
+                    <ul class="box-content-right-item">
+                        <li class="ul-title">SERVE | 服务</li>
+                        <li class="ul-item">售后服务</li>
+                        <li class="ul-item">维修服务</li>
+                        <li class="ul-item">保修服务</li>
+                    </ul>
+                    <ul class="box-content-right-item">
+                        <li class="ul-title">TEAM | 团队</li>
+                        <li class="ul-item">售后服务</li>
+                        <li class="ul-item">维修服务</li>
+                        <li class="ul-item">保修服务</li>
+                    </ul>
+                    <ul class="box-content-right-item">
+                        <li class="ul-title">CONTACT US | 关于我们</li>
+                        <li class="ul-item"> 叶凌辰《83534373@qq.com》 </li>
+                        <li class="ul-item">孙超《sunchao@qq.com》</li>
+                        <li class="ul-item">郑晓雪《zhengxiaoxue@qq.com》</li>
+                        <li class="ul-item">李峰纵《lifengzong@qq.com》</li>
+                        <li class="ul-item">VRONE群:85403524</li>
+                    </ul>
+                </div>
+            </div>
+
+            <div class="box-bottom">
+                <p> Copyright © 福州瀚智科技有限公司 All Rights Reserved 闽ICP备2022012099号</p>
+            </div>
+        </div>
+
+    </div>
+
+</template>
+
+<script setup>
+
+
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    display: flex;
+}
+
+.box-content {
+    flex: 1;
+    min-height: calc(10vw + 10vh + 10vmin);
+    height: 100%;
+    background-color: rgb(0, 0, 0);
+    background-image: url("@/assets/img/底部背景.png");
+    background-size: cover;
+    background-position: center;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+}
+
+.box-top {
+    flex: 1;
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+}
+
+.box-content-left {
+    flex: 1;
+    padding: calc(1.5vw + 1.5vh + 1.5vmin) calc(2vw + 2vh + 2vmin);
+    background-color: rgb(0, 0, 0, 0.9);
+}
+
+.box-content-left p:nth-child(1),
+.box-content-left p:nth-child(2) {
+    margin: 0;
+    font-size: 24px;
+    font-weight: 600;
+    color: rgb(255, 255, 255);
+    white-space: nowrap;
+    display: flex;
+    align-items: center;
+}
+
+.box-content-left-line {
+    width: 20px;
+    height: 20px;
+    margin-left: 8px;
+    border-radius: 50%;
+    background-color: #FD571F;
+}
+
+ul,
+li {
+    list-style: none;
+}
+
+.box-content-right {
+    flex: 11;
+    padding: calc(1.5vw + 1.5vh + 1.5vmin) 40px 0 70px;
+    color: rgb(255, 255, 255);
+    background-color: rgb(0, 0, 0, 0.9);
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
+}
+
+.ul-title {
+    margin-bottom: 16px;
+    font-size: 18px;
+    color: rgba(255, 255, 255, 1);
+    white-space: nowrap;
+}
+
+.ul-item {
+    position: relative;
+    padding: 8px 0;
+    font-size: 12px;
+    color: rgb(193 193 193);
+    width: fit-content;
+    border-bottom: 2px solid rgba(219, 219, 219, 0);
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 0.3s;
+    cursor: pointer;
+    white-space: nowrap;
+}
+
+.ul-item:hover {
+    color: #FD571F;
+    border-bottom: 2px solid #FD571F;
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 0.3s;
+}
+
+.box-bottom {
+    font-size: 12px;
+    padding-bottom: 16px;
+    color: rgb(255, 255, 255);
+    background-color: rgb(0, 0, 0, 0.9);
+    text-align: center;
+}
+
+/* 移动端样式 */
+@media screen and (max-width: 700px) {
+    .box-content-left {
+        display: flex;
+    }
+
+    .box-content-left p:nth-child(1) {
+        margin-right: 6px;
+    }
+
+    .box-content-right {
+        flex: 11;
+        padding: 0;
+        color: rgb(255, 255, 255);
+        background-color: rgb(0, 0, 0, 0.9);
+        display: grid;
+        grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
+    }
+
+    .box-content-right .box-content-right-item:nth-child(1),
+    .box-content-right .box-content-right-item:nth-child(4),
+    .box-content-right .box-content-right-item:nth-child(5) {
+        display: none;
+    }
+
+    .ul-title {
+        margin-bottom: 8px
+    }
+}
+</style>

+ 244 - 0
src/pages/Home/HomeComponents/tea_ceremony.vue

@@ -0,0 +1,244 @@
+<!-- 万里茶道 -->
+
+<template>
+
+    <div class="box">
+
+        <div class="box-content">
+
+            <!-- 左边 -->
+            <div class="box-content-left">
+                <div class="box-content-left-title position-relative">
+                    <span style="--i:1">万</span>
+                    <span style="--i:2">里</span>
+                    <span style="--i:3">茶</span>
+                    <span style="--i:4">道</span>
+                    <span style="--i:5">与</span>
+                    <span style="--i:6">历</span>
+                    <span style="--i:7">史</span>
+                    <span style="--i:8">的</span>
+                    <span style="--i:9">动</span>
+                    <span style="--i:10">脉</span>
+                </div>
+
+                <div class="box-content-left-englishTitle position-relative">
+                    <p>TEA ROAD AND THE ARTERIES</p>
+                    <p>OF HISTORY</p>
+                </div>
+
+                <div class="box-content-left-content position-relative">
+                    <p>指导单位: 中华文化促进会万里茶道协作体</p>
+                    <p>联合出品: 万里茶道实证研究中心</p>
+                    <p>&#8195&#8195&#8195&#8195&#8195 省广(福建)装饰设计工程有限公司</p>
+                    <p>&#8195&#8195&#8195&#8195&#8195 阳光学院元宇宙与新媒体学院</p>
+                    <p>&#8195&#8195&#8195&#8195&#8195 武汉工夫茶业有限公司</p>
+                </div>
+
+                <div class="box-content-left-endText position-relative">
+                    <p>万里茶道是一条起于福建武夷山,终点是俄罗斯圣彼得堡的茶叶贸易和文化交流通道</p>
+                </div>
+
+                <div class="box-content-left-img"></div>
+            </div>
+
+            <!-- 右边 -->
+            <div class="box-content-right">
+                <div class="box-content-right-content" @click="act_toTeaCeremony">
+                    <img src="@/assets/img/箭头.png" alt="">
+                    <p>Opening the Gateway to the Virtual World</p>
+                    <p>开启虚拟世界之门</p>
+                </div>
+            </div>
+
+        </div>
+
+    </div>
+
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+
+
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+
+
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+// 跳转页面
+const act_toTeaCeremony = () => {
+    window.open("http://img.han-zhi.net/wlcd/pv/index.html", "Ten-Thousand-Li-Tea-Route")
+}
+
+
+
+/************************************************
+ * 获取数据
+ * 
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    display: flex;
+}
+
+.box-content {
+    flex: 1;
+    min-height: calc(20vw + 20vh + 10vmin);
+    height: 100%;
+    background: linear-gradient(135deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.5) 100%);
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
+    justify-content: space-between;
+    align-items: flex-start;
+    grid-gap: 32px;
+}
+
+.box-content-left {
+    position: relative;
+    padding: calc(1.5vw + 1vh + 1vmin) 0 0 calc(2vw + 2vh + 2vmin);
+    overflow: hidden;
+}
+
+.box-content-left-title {
+    margin-bottom: 10vh;
+    font-size: 24px;
+    font-weight: 600;
+    color: rgba(253, 87, 31, 1);
+}
+
+.box-content:hover .box-content-left-title span {
+    /* 设置行内块元素 */
+    display: inline-block;
+    /* 添加动画 */
+    animation: donghua 1.5s ease-in-out;
+    /* 利用变量动态计算动画延迟时间 */
+    animation-delay: calc(.1s*var(--i));
+}
+
+@keyframes donghua {
+    0% {
+        transform: translateY(0px);
+    }
+
+    20% {
+        transform: translateY(-20px);
+    }
+
+    40%,
+    100% {
+        transform: translateY(0px);
+    }
+}
+
+.box-content-left-englishTitle {
+    margin: 0 calc(3vw + 3vh + 3vmin) 0 0;
+    font-size: 24px;
+    font-weight: 600;
+    color: rgba(255, 255, 255, 1);
+    border-bottom: 1px solid rgba(107, 107, 107, 1);
+}
+
+.box-content-left-content {
+    padding-top: 32px;
+    color: rgba(255, 255, 255, 1);
+    font-size: 14px;
+}
+
+.box-content-left-endText {
+    margin: 170px 0 64px 0;
+    color: rgba(255, 255, 255, 1);
+    font-size: 12px;
+}
+
+.position-relative {
+    position: relative;
+    z-index: 1;
+}
+
+.box-content-left:hover .box-content-left-img {
+    transform: scale(1.2);
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1.5s;
+    filter: hue-rotate(90deg) brightness(0.5);
+}
+
+.box-content-left-img {
+    position: absolute;
+    bottom: 0;
+    right: 64px;
+    width: calc(10vw + 10vh + 10vmin);
+    height: calc(10vw + 10vh + 10vmin);
+    background-image: url("@/assets/img/N.png");
+    background-size: cover;
+    background-position: center;
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+.box-content-right {
+    min-height: 500px;
+    height: 100%;
+    background-image: url("@/assets/img/万里茶道.png");
+    background-size: cover;
+    background-position: center;
+    background-repeat: no-repeat;
+}
+
+.box-content-right-content {
+    width: 100%;
+    height: 100%;
+    color: rgba(255, 255, 255, 1);
+    background-color: rgba(0, 0, 0, 0.2);
+    font-size: 16px;
+    text-align: center;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    opacity: 0;
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+    cursor: pointer;
+}
+
+.box-content-right-content img {
+    width: calc(1.5vw + 1vh + 1vmin);
+}
+
+.box-content-right-content p {
+    margin: 6px 0;
+    text-shadow: 2px 2px 5px rgba(255, 255, 255, 1);
+}
+
+.box-content-right:hover .box-content-right-content {
+    opacity: 1;
+    transition: cubic-bezier(0.075, 0.82, 0.165, 1) 1s;
+}
+
+/* 移动端样式 */
+@media screen and (max-width: 700px) {
+    .box-content-right-content {
+        opacity: 1;
+    }
+}
+</style>

+ 34 - 0
src/pages/Home/index.vue

@@ -0,0 +1,34 @@
+<template>
+
+    <div class="box">
+
+        <!-- 头部 -->
+        <headPage />
+
+        <!-- 基础介绍 -->
+        <basicIntroduction />
+
+        <!-- 近期项目 -->
+        <recentProject />
+
+        <!-- 万里茶道 -->
+        <teaCeremony />
+
+        <!-- 新闻资讯 -->
+        <newsInformation />
+
+
+    </div>
+
+</template>
+
+<script setup>
+import headPage from '@/pages/Home/HomeComponents/head.vue'
+import basicIntroduction from '@/pages/Home/HomeComponents/basic_introduction.vue'
+import recentProject from '@/pages/Home/HomeComponents/recent_project.vue'
+import teaCeremony from '@/pages/Home/HomeComponents/tea_ceremony.vue'
+import newsInformation from '@/pages/Home/HomeComponents/news_information.vue'
+
+</script>
+
+<style scoped></style>

+ 376 - 0
src/pages/Order/ConfirmOrder.vue

@@ -0,0 +1,376 @@
+<!-- 确认订单页面 -->
+
+<template>
+    <div class="confirm-order-page">
+        <div class="order-container">
+            <!-- 订单步骤 -->
+            <div class="order-steps">
+                <el-steps :active="1" finish-status="success">
+                    <el-step title="确认订单" />
+                    <el-step title="支付订单" />
+                    <el-step title="完成" />
+                </el-steps>
+            </div>
+
+            <!-- 订单内容 -->
+            <div class="order-content">
+                <!-- 商品信息 -->
+                <div class="order-section">
+                    <div class="section-header">
+                        <h3>商品信息</h3>
+                    </div>
+                    <div class="section-body">
+                        <el-table :data="g_templateCaseList" style="width: 100%">
+                            <el-table-column label="商品" min-width="400">
+                                <template #default="{ row }">
+                                    <div class="item-info">
+                                        <el-image
+                                            :src="(row.imgUrl?.indexOf('http') == 0 ? '' : 'https://') + row.imgUrl"
+                                            style="width: 80px; height: 80px" fit="cover" />
+                                        <div class="item-detail">
+                                            <div class="item-name">{{ row.name }}</div>
+                                            <div class="item-title">{{ row.title }}</div>
+                                            <div class="item-intro">{{ row.introduction }}</div>
+                                        </div>
+                                    </div>
+                                </template>
+                            </el-table-column>
+                            <el-table-column label="单价" min-width="120">
+                                <template #default="{ row }">
+                                    ¥{{ row.price.toFixed(2) }}
+                                </template>
+                            </el-table-column>
+                            <el-table-column label="数量" min-width="120">
+                                <template #default="{ row }">
+                                    {{ row.quantity || 1 }}
+                                </template>
+                            </el-table-column>
+                            <el-table-column label="小计" min-width="120">
+                                <template #default="{ row }">
+                                    ¥{{ ((row.price || 0) * (row.quantity || 1)).toFixed(2) }}
+                                </template>
+                            </el-table-column>
+                        </el-table>
+                    </div>
+                </div>
+
+                <!-- 订单信息 -->
+                <div class="order-section">
+                    <div class="section-header">
+                        <h3>订单信息</h3>
+                    </div>
+                    <div class="section-body">
+                        <el-form :model="orderForm" label-width="100px">
+                            <el-form-item label="备注">
+                                <el-input v-model="orderForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
+                            </el-form-item>
+                        </el-form>
+                    </div>
+                </div>
+
+                <!-- 订单金额 -->
+                <div class="order-section">
+                    <div class="section-header">
+                        <h3>订单金额</h3>
+                    </div>
+                    <div class="section-body">
+                        <div class="price-summary">
+                            <div class="price-item">
+                                <span>商品总价:</span>
+                                <span>¥{{ computeTotalAmount(g_templateCaseList).toFixed(2) }}</span>
+                            </div>
+                            <div class="price-item">
+                                <span>优惠金额:</span>
+                                <span>-¥0.00</span>
+                            </div>
+                            <div class="price-item total">
+                                <span>实付金额:</span>
+                                <span>¥{{ computeTotalAmount(g_templateCaseList).toFixed(2) }}</span>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 订单操作 -->
+            <div class="order-actions">
+                <el-button @click="goBack">返回修改</el-button>
+                <el-button type="primary" @click="confirmOrderClick" :loading="submitting">
+                    提交订单
+                </el-button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, watch, onMounted } from 'vue'
+import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
+import { useRoute, useRouter } from 'vue-router'
+import { useUserStore } from '@/store/index'
+import { generateOrder } from '@/http/api/pages/Order/index'
+import { getTemplatesDetail } from '@/http/api/pages/Template/index'
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+const g_route = useRoute();
+const g_router = useRouter();
+const g_userStore = useUserStore();
+const g_templateCaseList = ref([]);
+
+const orderForm = ref({
+    invoiceType: '1',
+    remark: ''
+});
+
+const submitting = ref(false);
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+onMounted(() => {
+    const templateId = g_route.query.templateId;
+    const cartItems = g_route.query.items;
+
+    if (templateId) {
+        // 直接购买场景
+        getTemplateCase(templateId);
+    } else if (cartItems) {
+        // 购物车结算场景
+        try {
+            g_templateCaseList.value = JSON.parse(cartItems);
+        } catch (error) {
+            ElMessage.error('订单数据解析错误');
+            g_router.push('/cart');
+        }
+    } else {
+        ElMessage.error('订单数据错误');
+        g_router.push('/');
+    }
+})
+
+/************************************************
+ * 获取数据
+ *
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+const getTemplateCase = async (templateId) => {
+    var response = await getTemplatesDetail(templateId);
+    g_templateCaseList.value = [response.data];
+}
+
+const computeTotalAmount = function (templateCaseList = []) {
+    return templateCaseList.reduce((result, item) => {
+        return result + Number(item.price || 0) * (item.quantity || 1);
+    }, 0);
+}
+
+const formatDate = function (date, format = 'YYYY-MM-DD HH:mm:ss') {
+    const pad = (num) => num.toString().padStart(2, '0');
+    return format
+        .replace(/YYYY/g, date.getFullYear())
+        .replace(/MM/g, pad(date.getMonth() + 1))
+        .replace(/DD/g, pad(date.getDate()))
+        .replace(/HH/g, pad(date.getHours()))
+        .replace(/mm/g, pad(date.getMinutes()))
+        .replace(/ss/g, pad(date.getSeconds()));
+}
+
+const getOrderCreatePayload = function (templateCaseList = []) {
+    return {
+        orders: {
+            userId: g_userStore.userInfo.user.id,
+            orderDate: formatDate(new Date(), "YYYY-MM-DD"),
+            totalAmount: computeTotalAmount(templateCaseList),
+            orderStatus: null,
+            refundStatus: null,
+            refundDate: null,
+        },
+        detailList: templateCaseList.reduce((result, item) => {
+            result.push({
+                templateId: item.id,
+                quantity: item.quantity || 1,
+                price: item.price,
+                createTime: formatDate(new Date()),
+                status: 1,
+            });
+            return result;
+        }, [])
+    }
+}
+
+/************************************************
+* 事件处理
+*
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+const confirmOrderClick = function () {
+    if (!g_userStore.isUserLogin()) {
+        ElMessage.warning('请先登录');
+        g_router.push('/login');
+        return;
+    }
+
+    var createPayload = getOrderCreatePayload(g_templateCaseList.value);
+    console.log(createPayload)
+    createOrderAction(createPayload);
+}
+
+const createOrderAction = async (payload) => {
+    const loading = ElLoading.service({ fullscreen: true, text: "订单正在生成中..." });
+    try {
+        var response = await generateOrder(payload);
+        loading.close();
+        if (response.code == 200) {
+            // 跳转到订单支付页面
+            g_router.push({
+                path: '/PayOrder',
+                query: {
+                    orderId: String(response.data.id)
+                }
+            });
+        } else {
+            ElMessageBox.alert(`订单生成失败!请稍候再试!`, '');
+        }
+    } catch (error) {
+        loading.close();
+        ElMessageBox.alert(`订单生成失败!请稍候再试!`, '');
+    }
+}
+
+const goBack = () => {
+    g_router.back();
+};
+
+</script>
+
+<style scoped>
+.confirm-order-page {
+    min-height: calc(100vh - 60px);
+    background-color: #f5f7fa;
+    padding: 20px;
+}
+
+.order-container {
+    max-width: 1200px;
+    margin: 0 auto;
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+    padding: 20px;
+}
+
+.order-steps {
+    margin-bottom: 30px;
+    padding: 20px;
+    background-color: #fff;
+    border-radius: 8px;
+}
+
+.order-section {
+    margin-bottom: 20px;
+    border: 1px solid #ebeef5;
+    border-radius: 8px;
+    overflow: hidden;
+}
+
+.section-header {
+    padding: 15px 20px;
+    background-color: #f5f7fa;
+    border-bottom: 1px solid #ebeef5;
+}
+
+.section-header h3 {
+    margin: 0;
+    font-size: 16px;
+    color: #303133;
+}
+
+.section-body {
+    padding: 20px;
+}
+
+.item-info {
+    display: flex;
+    align-items: center;
+    gap: 15px;
+}
+
+.item-detail {
+    flex: 1;
+}
+
+.item-name {
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 5px;
+}
+
+.item-title {
+    font-size: 14px;
+    color: #606266;
+    margin-bottom: 5px;
+}
+
+.item-intro {
+    font-size: 12px;
+    color: #909399;
+    line-height: 1.5;
+}
+
+.price-summary {
+    text-align: right;
+    padding: 0 20px;
+}
+
+.price-item {
+    margin-bottom: 10px;
+    font-size: 14px;
+    color: #606266;
+}
+
+.price-item.total {
+    font-size: 18px;
+    font-weight: bold;
+    color: #f56c6c;
+    margin-top: 10px;
+    padding-top: 10px;
+    border-top: 1px dashed #dcdfe6;
+}
+
+.order-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 15px;
+    margin-top: 30px;
+    padding-top: 20px;
+    border-top: 1px solid #ebeef5;
+}
+
+/* 响应式布局 */
+@media screen and (max-width: 768px) {
+    .order-container {
+        padding: 15px;
+    }
+
+    .section-body {
+        padding: 15px;
+    }
+
+    .item-info {
+        flex-direction: column;
+        align-items: flex-start;
+    }
+
+    .item-detail {
+        width: 100%;
+    }
+}
+</style>

+ 249 - 0
src/pages/Order/OrderSuccess.vue

@@ -0,0 +1,249 @@
+<template>
+    <div class="order-success-page">
+        <div class="success-container">
+            <!-- 订单步骤 -->
+            <div class="order-steps">
+                <el-steps :active="3" finish-status="success">
+                    <el-step title="确认订单" />
+                    <el-step title="支付订单" />
+                    <el-step title="完成" />
+                </el-steps>
+            </div>
+
+            <!-- 成功提示 -->
+            <div class="success-content">
+                <div class="success-icon">
+                    <el-icon>
+                        <CircleCheck />
+                    </el-icon>
+                </div>
+                <h2>订单支付成功!</h2>
+                <p class="success-tip">您的订单已成功支付,我们将尽快为您处理</p>
+
+                <!-- 订单信息 -->
+                <div class="order-info">
+                    <div class="info-item">
+                        <span class="label">订单编号:</span>
+                        <span class="value">{{ orderInfo.id }}</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="label">支付金额:</span>
+                        <span class="value price">¥{{ orderInfo.totalAmount?.toFixed(2) || '0.00' }}</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="label">支付时间:</span>
+                        <span class="value">{{ formatTime(orderInfo.payTime) }}</span>
+                    </div>
+                </div>
+
+                <!-- 操作按钮 -->
+                <div class="action-buttons">
+                    <el-button @click="viewOrder">查看订单</el-button>
+                    <el-button type="primary" @click="goHome">返回首页</el-button>
+                </div>
+
+                <!-- 温馨提示 -->
+                <div class="tips-section">
+                    <h3>温馨提示</h3>
+                    <ul class="tips-list">
+                        <li>您可以在"我的订单"中查看订单详情</li>
+                        <li>如有任何问题,请联系客服</li>
+                        <li>感谢您的支持,祝您使用愉快!</li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { CircleCheck } from '@element-plus/icons-vue'
+import { getOrderDetail } from '@/http/api/pages/Order/index'
+import { usePersonalCenterStore } from "@/store/index";
+
+const g_personalCenterStore = usePersonalCenterStore();
+
+const route = useRoute()
+const g_router = useRouter()
+const orderInfo = ref({})
+
+onMounted(async () => {
+    const orderId = route.query.orderId
+    if (orderId) {
+        const response = await getOrderDetail(orderId)
+        orderInfo.value = response.data
+    }
+})
+
+const formatTime = (time) => {
+    if (!time) return new Date().toLocaleString()
+    return new Date(time).toLocaleString()
+}
+
+const viewOrder = () => {
+    g_personalCenterStore.setActiveTabName('myOrder');
+    g_router.push({ path: "/personalCenter", query: { tab: 'myOrder' } })
+}
+
+const goHome = () => {
+    g_router.push({ name: '首页' });
+}
+</script>
+
+<style scoped>
+.order-success-page {
+    min-height: calc(100vh - 60px);
+    background-color: #f5f7fa;
+    padding: 20px;
+}
+
+.success-container {
+    max-width: 800px;
+    margin: 0 auto;
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+    padding: 20px;
+}
+
+.order-steps {
+    margin-bottom: 30px;
+    padding: 20px;
+    background-color: #fff;
+    border-radius: 8px;
+}
+
+.success-content {
+    text-align: center;
+    padding: 40px 20px;
+}
+
+.success-icon {
+    margin-bottom: 20px;
+}
+
+.success-icon .el-icon {
+    font-size: 64px;
+    color: #67c23a;
+}
+
+.success-content h2 {
+    font-size: 24px;
+    color: #303133;
+    margin-bottom: 10px;
+}
+
+.success-tip {
+    font-size: 16px;
+    color: #606266;
+    margin-bottom: 30px;
+}
+
+.order-info {
+    background-color: #f5f7fa;
+    border-radius: 8px;
+    padding: 20px;
+    margin-bottom: 30px;
+    text-align: left;
+}
+
+.info-item {
+    margin-bottom: 15px;
+    display: flex;
+    align-items: center;
+}
+
+.info-item:last-child {
+    margin-bottom: 0;
+}
+
+.label {
+    width: 100px;
+    color: #606266;
+    font-size: 14px;
+}
+
+.value {
+    flex: 1;
+    color: #303133;
+    font-size: 14px;
+}
+
+.value.price {
+    color: #f56c6c;
+    font-size: 18px;
+    font-weight: bold;
+}
+
+.action-buttons {
+    display: flex;
+    justify-content: center;
+    gap: 15px;
+    margin-bottom: 30px;
+}
+
+.tips-section {
+    text-align: left;
+    padding: 20px;
+    background-color: #f5f7fa;
+    border-radius: 8px;
+}
+
+.tips-section h3 {
+    font-size: 16px;
+    color: #303133;
+    margin-bottom: 15px;
+}
+
+.tips-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
+.tips-list li {
+    color: #606266;
+    font-size: 14px;
+    line-height: 1.8;
+    position: relative;
+    padding-left: 20px;
+}
+
+.tips-list li::before {
+    content: "•";
+    position: absolute;
+    left: 0;
+    color: #67c23a;
+}
+
+/* 响应式布局 */
+@media screen and (max-width: 768px) {
+    .success-container {
+        padding: 15px;
+    }
+
+    .success-content {
+        padding: 20px;
+    }
+
+    .info-item {
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 5px;
+    }
+
+    .label {
+        width: auto;
+    }
+
+    .action-buttons {
+        flex-direction: column;
+    }
+
+    .action-buttons .el-button {
+        width: 100%;
+    }
+}
+</style>

+ 256 - 0
src/pages/Order/PayOrder.vue

@@ -0,0 +1,256 @@
+<template>
+    <div class="pay-order-page">
+        <div class="order-container">
+            <!-- 订单步骤 -->
+            <div class="order-steps">
+                <el-steps :active="2" finish-status="success">
+                    <el-step title="确认订单" />
+                    <el-step title="支付订单" />
+                    <el-step title="完成" />
+                </el-steps>
+            </div>
+
+            <!-- 订单内容 -->
+            <div class="order-content">
+                <!-- 订单信息 -->
+                <div class="order-section">
+                    <div class="section-header">
+                        <h3>订单信息</h3>
+                    </div>
+                    <div class="section-body">
+                        <div class="info-item">
+                            <span class="label">订单编号:</span>
+                            <span class="value">{{ g_order.id }}</span>
+                        </div>
+                        <div class="info-item">
+                            <span class="label">订单金额:</span>
+                            <span class="value price">¥{{ g_order.totalAmount?.toFixed(2) || '0.00' }}</span>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 钱包信息 -->
+                <div class="order-section">
+                    <div class="section-header">
+                        <h3>钱包信息</h3>
+                    </div>
+                    <div class="section-body">
+                        <div class="info-item">
+                            <span class="label">钱包余额:</span>
+                            <span class="value price">¥{{ g_userStore.userInfo.user.money?.toFixed(2) || '0.00'
+                                }}</span>
+                        </div>
+                        <div class="balance-tip" v-if="g_userStore.userInfo.user.money < g_order.totalAmount">
+                            <el-icon>
+                                <Warning />
+                            </el-icon>
+                            <span>余额不足,请先充值</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 支付操作 -->
+            <div class="order-actions">
+                <el-button @click="goBack">返回修改</el-button>
+                <el-button type="primary" :disabled="g_userStore.userInfo.user.money < g_order.totalAmount"
+                    @click="payOrderClick" :loading="submitting">
+                    确认支付
+                </el-button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { useRoute, useRouter } from 'vue-router'
+import { useUserStore } from '@/store/index'
+import { getOrderDetail, payOrder } from '@/http/api/pages/Order/index'
+import { Warning } from '@element-plus/icons-vue'
+
+const g_route = useRoute();
+const g_router = useRouter();
+const g_userStore = useUserStore();
+const g_order = ref({});
+const submitting = ref(false);
+
+onMounted(async () => {
+    const orderId = g_route.query.orderId;
+    if (!orderId) {
+        ElMessage.error('订单ID不存在,请重新进入');
+        g_router.push({ name: '首页' });
+        return;
+    }
+
+    try {
+        const response = await getOrderDetail(orderId);
+        if (response.data) {
+            g_order.value = response.data;
+        } else {
+            ElMessage.error('订单信息获取失败,请刷新页面重试');
+        }
+    } catch (error) {
+
+        console.error('获取订单信息失败:', error);
+        ElMessage.error('获取订单信息失败,请刷新页面重试');
+    }
+})
+
+const payOrderClick = async () => {
+    // 检查订单数据是否存在
+    if (!g_order.value || !g_order.value.totalAmount) {
+        ElMessage.error('订单信息获取失败,请刷新页面重试');
+        return;
+    }
+
+    // 检查用户钱包余额
+    if (g_userStore.userInfo.user.money < g_order.value.totalAmount) {
+        ElMessage.error('钱包余额不足,请先充值!');
+        return;
+    }
+
+    submitting.value = true;
+    try {
+        // TODO: 调用支付接口
+        var response = await payOrder({ orderId: g_order.value.id });
+        if (response.code == 200) {
+            // 支付成功后跳转到订单完成页面
+            g_router.push({
+                name: 'OrderSuccess',
+                query: {
+                    orderId: g_order.value.id
+                }
+            });
+        } else {
+            ElMessageBox.alert(`订单支付失败!请稍候再试!`, '');
+        }
+    } catch (error) {
+        console.error('支付失败:', error);
+        ElMessage.error('支付失败,请重试!');
+    } finally {
+        submitting.value = false;
+    }
+};
+
+const goBack = () => {
+    g_router.back();
+};
+</script>
+
+<style scoped>
+.pay-order-page {
+    min-height: calc(100vh - 60px);
+    background-color: #f5f7fa;
+    padding: 20px;
+}
+
+.order-container {
+    max-width: 1200px;
+    margin: 0 auto;
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+    padding: 20px;
+}
+
+.order-steps {
+    margin-bottom: 30px;
+    padding: 20px;
+    background-color: #fff;
+    border-radius: 8px;
+}
+
+.order-section {
+    margin-bottom: 20px;
+    border: 1px solid #ebeef5;
+    border-radius: 8px;
+    overflow: hidden;
+}
+
+.section-header {
+    padding: 15px 20px;
+    background-color: #f5f7fa;
+    border-bottom: 1px solid #ebeef5;
+}
+
+.section-header h3 {
+    margin: 0;
+    font-size: 16px;
+    color: #303133;
+}
+
+.section-body {
+    padding: 20px;
+}
+
+.info-item {
+    margin-bottom: 15px;
+    display: flex;
+    align-items: center;
+}
+
+.info-item:last-child {
+    margin-bottom: 0;
+}
+
+.label {
+    width: 100px;
+    color: #606266;
+    font-size: 14px;
+}
+
+.value {
+    flex: 1;
+    color: #303133;
+    font-size: 14px;
+}
+
+.value.price {
+    color: #f56c6c;
+    font-size: 18px;
+    font-weight: bold;
+}
+
+.balance-tip {
+    margin-top: 15px;
+    padding: 10px;
+    background-color: #fef0f0;
+    border-radius: 4px;
+    color: #f56c6c;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.order-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 15px;
+    margin-top: 30px;
+    padding-top: 20px;
+    border-top: 1px solid #ebeef5;
+}
+
+/* 响应式布局 */
+@media screen and (max-width: 768px) {
+    .order-container {
+        padding: 15px;
+    }
+
+    .section-body {
+        padding: 15px;
+    }
+
+    .info-item {
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 5px;
+    }
+
+    .label {
+        width: auto;
+    }
+}
+</style>

ファイルの差分が大きいため隠しています
+ 442 - 0
src/pages/Template/templateCase.vue


+ 395 - 0
src/pages/User/UserComponents/CreateEditPage/CreateEditDataSources.vue

@@ -0,0 +1,395 @@
+<!-- 数据源页面 -->
+
+<template>
+    <div>
+        <div class="buttonSet">
+            <el-button type="primary" @click="act_dataSourceOperation(1)">创建</el-button>
+        </div>
+
+        <!-- 表格数据 -->
+        <el-table :data="g_SpotTableData" border table-layout="fixed" empty-text="暂无数据">
+            <el-table-column prop="title" label="名称" />
+            <el-table-column prop="typeName" label="类型" />
+
+            <el-table-column label="封面">
+                <template #default="scope">
+                    <el-image class="avatar"
+                        :src="(scope.row.cover?.indexOf('http') == 0 ? '' : 'https://') + scope.row.cover"
+                        fit="cover" />
+                </template>
+            </el-table-column>
+
+            <el-table-column label="操作">
+                <template #default="scope">
+                    <el-button type="warning" @click="act_dataSourceOperation(2, scope.row)">编辑</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+
+        <!-- 选择展厅抽屉 -->
+        <el-drawer v-if="g_innerDrawer" v-model="g_innerDrawer" :title="g_drawerName" :append-to-body="true" size="45%"
+            style="min-width: 350px;" :before-close="act_closeCallback" modal-class="dialog_class">
+            <el-form :model="g_form" label-width="auto">
+                <el-form-item label="名称">
+                    <el-input v-model="g_form.dataSource.title" />
+                </el-form-item>
+
+                <el-form-item label="类型">
+                    <el-select v-model="g_form.dataSource.type" placeholder="Select" size="large" style="width: 100%"
+                        @change="act_dataSourceTypeChange">
+                        <el-option v-for="item in g_dataSourceType" :key="item.id" :label="item.title"
+                            :value="item.id" />
+                    </el-select>
+                </el-form-item>
+
+                <!-- <el-form-item label="配置">
+                    <el-input v-model="g_form.config" />
+                </el-form-item> -->
+
+                <el-form-item label="数据">
+                    <!-- 图片类型 -->
+                    <ImageUpload v-if="g_form.dataSource.type == 1" ref="imgchildComponentRef_1"
+                        :URL="(g_form.data?.indexOf('http') == 0 ? '' : 'https://') + g_form.data"
+                        :handleUploadSuccess="act_handleUploadSuccess" />
+                    <!-- 视频类型 -->
+                    <VideoUpload v-if="g_form.dataSource.type == 2" ref="videochildComponentRef" :URL="g_form.data"
+                        :handleUploadSuccess="act_handleUploadSuccess" />
+                    <!-- 超链接、文字类型 -->
+                    <el-input v-if="g_form.dataSource.type == 3 || g_form.dataSource.type == 4" v-model="g_form.data" />
+                </el-form-item>
+
+                <el-form-item label="封面">
+                    <ImageUpload ref="imgchildComponentRef_2"
+                        :URL="(g_form.cover?.indexOf('http') == 0 ? '' : 'https://') + g_form.cover"
+                        :handleUploadSuccess="act_coverUploadSuccess" />
+                </el-form-item>
+
+                <el-form-item>
+                    <el-button type="primary" @click="act_onSubmit">{{ g_drawerType == 2 ? "修改" : "创建" }}</el-button>
+                </el-form-item>
+            </el-form>
+        </el-drawer>
+
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessageBox, ElMessage, ElLoading } from 'element-plus'
+import ImageUpload from '@/components/Common/ImageUpload.vue'
+import VideoUpload from '@/components/Common/VideoUpload.vue'
+import { getGalleryDataSources, updateDataSource, addDataSource } from '@/http/api/pages/DataSources/index'
+import { getDataType } from '@/http/api/pages/DataType/index'
+
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+
+// 父组件传递过来的函数
+const g_props = defineProps({
+    act_updateVisibility: Function,
+    get_userAppDataList: Function,
+    g_ShowroomDataList: Object,
+})
+
+// 子组件引用
+const imgchildComponentRef_1 = ref(null);
+const imgchildComponentRef_2 = ref(null);
+const videochildComponentRef = ref(null);
+
+// 表格数据
+const g_SpotTableData = ref([])
+
+
+// 页面类型 如果有id 则是编辑页面 如果没有id 则是新增页面
+const g_pageType = ref(g_props.g_ShowroomDataList.id)
+
+// 选择展厅抽屉状态
+const g_innerDrawer = ref(false)
+// 抽屉类型
+const g_drawerType = ref(1)
+// 抽屉名称
+const g_drawerName = ref("数据源创建")
+// 数据源类型
+const g_dataSourceType = [
+    {
+        "id": 1,
+        "title": "图片",
+        "introduction": "图片类型",
+    },
+    {
+        "id": 2,
+        "title": "视频",
+        "introduction": "视频类型",
+    },
+    {
+        "id": 3,
+        "title": "超链接",
+        "introduction": "超链接类型",
+    },
+    {
+        "id": 4,
+        "title": "文字",
+        "introduction": "文字类型",
+    },
+    {
+        "id": 5,
+        "title": "图册",
+        "introduction": "图册类型",
+    },
+    {
+        "id": 6,
+        "title": "模型",
+        "introduction": "模型类型",
+    }
+]
+
+// 编辑、创建数据
+const g_form = reactive({
+    dataSource: {
+        id: "", // 数据源id
+        appId: "", // 展厅id
+        spot: "", // 展示点
+        title: "", // 数据源名称
+        type: "", // 数据源类型
+    },
+    data: "", // 图片/视频数据url
+    cover: "", // 图片缩略图/视频封面url
+})
+
+
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+    act_onInit();
+})
+
+
+
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+// 初始化页面
+const act_onInit = () => {
+    if (g_pageType.value) {
+        get_SceneDetail();
+    }
+}
+
+// 打开抽屉
+const act_dataSourceOperation = (type, row) => {
+    g_drawerName.value = type == 1 ? "数据源创建" : "数据源编辑"
+    g_drawerType.value = type
+    g_innerDrawer.value = true
+    act_dataSourceInit(row);
+}
+
+// 初始化抽屉信息
+const act_dataSourceInit = (row) => {
+    const isCreate = g_drawerType.value == 1
+    g_form.dataSource.id = isCreate ? "" : row.id;
+    g_form.dataSource.appId = g_pageType.value;
+    g_form.dataSource.spot = isCreate ? "" : row.spot;
+    g_form.dataSource.title = isCreate ? "" : row.title;
+    g_form.dataSource.type = isCreate ? "" : row.type;
+    g_form.cover = isCreate ? "" : row.cover;
+    g_form.data = isCreate ? "" : row.url;
+    console.log("初始化抽屉信息", g_form)
+}
+
+// 数据源类型切换
+const act_dataSourceTypeChange = (val) => {
+    // console.log(g_form, val)
+}
+
+// 数据成功回调
+const act_handleUploadSuccess = (res, type) => {
+    g_form.data = res.url
+}
+
+// 封面上传成功回调
+const act_coverUploadSuccess = (res) => {
+    g_form.cover = res.url
+}
+
+// 提交
+const act_onSubmit = async () => {
+    if (!g_form.dataSource.title) {
+        return ElMessage({
+            message: '请输入展厅名称',
+            grouping: true,
+            type: 'warning',
+        });
+    }
+
+    if (!g_form.dataSource.type) {
+        return ElMessage({
+            message: '请选中数据源类型',
+            grouping: true,
+            type: 'warning',
+        });
+    }
+
+    let loadingInstance;
+
+    try {
+        await ElMessageBox.confirm(
+            `${g_drawerType.value == 2 ? "确定修改该数据源吗" : "确定创建该数据源吗"}`,
+            '温馨提示',
+            {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning',
+            }
+        )
+
+        loadingInstance = ElLoading.service({
+            lock: true,
+            text: '上传中,请稍候...',
+            background: 'rgba(0, 0, 0, 0.7)',
+        });
+
+        if (imgchildComponentRef_1.value && imgchildComponentRef_1.value.isFileChanged) {
+            await imgchildComponentRef_1.value.act_uploadFiles();
+        }
+
+        if (imgchildComponentRef_2.value && imgchildComponentRef_2.value.isFileChanged) {
+            await imgchildComponentRef_2.value.act_uploadFiles();
+        }
+
+        if (videochildComponentRef.value && videochildComponentRef.value.isFileChanged) {
+            await videochildComponentRef.value.act_uploadFiles();
+        }
+
+        g_drawerType.value == 2 ? act_updateDataSource() : act_addDataSource();
+
+    } catch (error) {
+        console.log("操作被取消", error);
+    } finally {
+        if (loadingInstance) {
+            loadingInstance.close();
+        }
+    }
+
+}
+
+// 修改数据源
+const act_updateDataSource = () => {
+    updateDataSource(g_form).then((res) => {
+        if (res.code == 200) {
+            ElMessage({
+                message: '修改成功',
+                grouping: true,
+                type: 'success',
+            });
+            g_SpotTableData.value.find(item => {
+                if (item.id == g_form.dataSource.id) {
+                    item.title = g_form.dataSource.title;
+                    item.type = g_form.dataSource.type;
+                    item.url = g_form.data;
+                    item.cover = g_form.cover;
+                }
+            })
+            g_innerDrawer.value = false;
+        }
+    })
+}
+
+// 新增数据源
+const act_addDataSource = async () => {
+    try {
+        const res = await addDataSource(g_form);
+        if (res.code == 200) {
+            ElMessage({
+                message: '创建成功',
+                grouping: true,
+                type: 'success',
+            });
+            get_SceneDetail();
+        } else {
+            ElMessage({
+                message: res.msg,
+                grouping: true,
+                type: 'warning',
+            });
+        }
+    } catch (err) {
+        ElMessage.error('创建失败!');
+    } finally {
+        g_innerDrawer.value = false;
+    }
+}
+
+// 关闭回调
+const act_closeCallback = (done) => {
+    ElMessageBox.confirm(
+        '确定关闭吗?',
+        '温馨提示',
+        {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning',
+        }
+    ).then(() => {
+        done();
+    }).catch(() => {
+
+    })
+}
+
+/************************************************
+ * 获取数据
+ *
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+// 根据展厅id查询数据源列表
+const get_SceneDetail = () => {
+    getGalleryDataSources(g_pageType.value).then(res => {
+        res.data.forEach(item => {
+            item["typeName"] = g_dataSourceType.find(i => i.id == item.type).title;
+        })
+        g_SpotTableData.value = res.data
+    })
+}
+
+
+
+</script>
+
+<style scoped>
+.buttonSet {
+    margin-bottom: 8px
+}
+
+::v-deep .el-input {
+    flex: 1;
+}
+
+::v-deep .el-form-item__content {
+    display: flex;
+    justify-content: center;
+}
+</style>
+
+
+<style>
+.dialog_class {
+    background-color: rgb(0, 0, 0, 0) !important;
+}
+</style>

+ 415 - 0
src/pages/User/UserComponents/CreateEditPage/CreateEditScene.vue

@@ -0,0 +1,415 @@
+<!-- 创建场景&修改场景 -->
+
+<template>
+    <div>
+        <!-- 表单数据 -->
+        <el-form v-if="g_isLoaded" :model="g_form" label-width="auto">
+            <el-form-item label="归属展厅">
+                <el-input v-model="g_form.appName" disabled />
+                <el-button type="primary" style="margin-left: 10px;"
+                    @click="act_openSecondaryDrawer(1)">选择展厅</el-button>
+            </el-form-item>
+
+            <el-form-item label="场景名称">
+                <el-input v-model="g_form.title" />
+            </el-form-item>
+
+            <el-form-item label="挂载模板">
+                <el-input v-model="g_form.templateName" disabled />
+                <el-button v-if="!g_pageType" type="primary" style="margin-left: 10px;"
+                    @click="act_openSecondaryDrawer(2)">选择模板</el-button>
+            </el-form-item>
+
+            <el-form-item label="场景封面">
+                <ImageUpload ref="imgchildComponentRef"
+                    :URL="(g_form.cover?.indexOf('http') == 0 ? '' : 'https://') + g_form.cover"
+                    :handleUploadSuccess="act_handleUploadSuccess" />
+            </el-form-item>
+
+            <el-form-item>
+                <el-button type="primary" @click="act_onSubmit">{{ g_pageType ? "修改" : "创建" }}</el-button>
+            </el-form-item>
+        </el-form>
+
+        <!-- 选择展厅抽屉 -->
+        <el-drawer v-model="g_innerDrawer" :title="g_secondaryDrawerType == 1 ? '选择展厅' : '选择模板'" :append-to-body="true"
+            size="45%" style="min-width: 350px;" modal-class="dialog_class">
+
+            <!-- 查询展厅 -->
+            <el-form v-if="g_secondaryDrawerType == 1" class="form-custom">
+                <el-form-item label="展厅名称">
+                    <el-input v-model="g_queryCondition.title" />
+                    <el-button type="primary" style="margin-left: 10px;" @click="act_inquire">查询</el-button>
+                </el-form-item>
+
+                <!-- 表格数据 -->
+                <TablePaging :data="g_galleryTableData" :currentChange="act_handleCurrentChange">
+                    <template #table>
+                        <el-table-column prop="title" label="展厅名称" width="180" />
+                        <el-table-column prop="introduction" label="简介" />
+                    </template>
+                </TablePaging>
+
+                <el-form-item>
+                    <el-button type="primary" @click="act_ensureGallert">确定</el-button>
+                </el-form-item>
+            </el-form>
+
+
+            <!-- 查询模板 -->
+            <el-form v-if="g_secondaryDrawerType == 2" class="form-custom">
+                <el-form-item label="模板名称">
+                    <el-input v-model="g_queryCondition.title" />
+                    <el-button type="primary" style="margin-left: 10px;" @click="act_inquire">查询</el-button>
+                </el-form-item>
+
+                <!-- 表格数据 -->
+                <TablePaging :data="g_TemplateTableData" :currentChange="act_handleCurrentChange">
+                    <template #table>
+                        <el-table-column prop="title" label="模板名称" width="180" />
+                        <el-table-column prop="introduction" label="简介" />
+                    </template>
+                </TablePaging>
+
+                <el-form-item>
+                    <el-button type="success" @click="act_ensureGallert">确定</el-button>
+                </el-form-item>
+            </el-form>
+        </el-drawer>
+
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessageBox, ElMessage, ElLoading } from 'element-plus'
+import { getScenesDetail, updateScenes, addScenes } from '@/http/api/pages/Scene/index'
+import { getAppsDetail, getAppsList, getUserAppsList } from '@/http/api/pages/Gallery/index'
+import { getTemplatesList, getTemplatesDetail, getTemplatesBuyList } from '@/http/api/pages/Template/index'
+import ImageUpload from '@/components/Common/ImageUpload.vue'
+import TablePaging from '@/components/Common/TablePaging.vue'
+import { useMySceneStore } from "@/store/index";
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 获取我的场景
+const g_useMySceneStore = useMySceneStore();
+
+// 父组件传递过来的函数
+const g_props = defineProps({
+    act_updateVisibility: Function,
+    get_userScenesDataList: Function,
+    g_SceneDataList: Object,
+})
+
+// 子组件引用
+const imgchildComponentRef = ref(null);
+
+// 页面类型 如果有id 则是编辑页面 如果没有id 则是新增页面
+const g_pageType = ref(g_props.g_SceneDataList.id)
+
+// 查询条件表单
+const g_queryCondition = reactive({
+    title: "", // 展厅\模板名称
+    userId: g_props.g_SceneDataList.userId, // 用户id
+})
+
+// 待确认表单
+const g_confirmedForm = reactive({
+    appName: "", // 展厅名称
+    appId: "",// 展厅id
+    templateName: "", // 模板名称
+    templateId: "", // 模板id
+})
+
+// 表单数据
+const g_form = reactive({
+    id: g_props.g_SceneDataList.id,
+    appName: "", // 展厅名称
+    appId: "", // 展厅id
+    cover: "", // 场景缩略图
+    title: "", // 场景名称
+    spot: "", // 展示点
+    templateName: "", // 模板名称
+    templateId: "", // 模板id
+    type: "", // 类型
+    status: "", // 状态
+    userId: g_props.g_SceneDataList.userId
+})
+
+// 二级抽屉类型
+const g_secondaryDrawerType = ref(1)
+// 选择展厅抽屉状态
+const g_innerDrawer = ref(false)
+
+// 展厅表格数据
+const g_galleryTableData = ref([])
+const g_TemplateTableData = ref([])
+
+// 加载状态
+const g_isLoaded = ref(false)
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+    act_onInit();
+})
+
+
+
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+// 初始化页面
+const act_onInit = () => {
+    if (g_pageType.value) {
+        getSceneDetail();
+    } else {
+        g_isLoaded.value = true;
+    }
+}
+
+// 提交
+const act_onSubmit = async () => {
+    if (!g_form.appId) {
+        ElMessage({
+            message: '请选择归属展厅',
+            grouping: true,
+            type: 'warning',
+        })
+        return
+    }
+
+    if (!g_form.title) {
+        ElMessage({
+            message: '请输入场景名称',
+            grouping: true,
+            type: 'warning',
+        })
+        return
+    }
+
+    if (!g_form.templateId) {
+        ElMessage({
+            message: '请选择挂载模板',
+            grouping: true,
+            type: 'warning',
+        })
+        return
+    }
+
+    let loadingInstance;
+
+    try {
+        await ElMessageBox.confirm(
+            `${g_pageType.value ? "确定修改该场景吗" : "确定创建该场景吗"}`,
+            '温馨提示',
+            {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning',
+            }
+        );
+
+        loadingInstance = ElLoading.service({
+            lock: true,
+            text: '上传中,请稍候...',
+            background: 'rgba(0, 0, 0, 0.7)',
+        });
+
+        if (imgchildComponentRef.value && imgchildComponentRef.value.isFileChanged) {
+            await imgchildComponentRef.value.act_uploadFiles();
+        }
+
+        g_pageType.value ? act_updateScene() : act_addScene();
+        g_props.act_updateVisibility();
+    } catch (error) {
+        console.log("操作被取消", error);
+    } finally {
+        if (loadingInstance) {
+            loadingInstance.close();
+        }
+    }
+}
+
+// 上传成功回调
+const act_handleUploadSuccess = (res, type) => {
+    type == "img" ? g_form.cover = res.url : g_form.video = res.url
+}
+
+// 查询
+const act_inquire = () => {
+    if (!g_queryCondition.title) {
+        get_userScenesDataList()
+        return ElMessage({
+            message: '请输入查询条件',
+            grouping: true,
+            type: 'warning',
+        })
+    }
+    get_queryDataList();
+}
+
+// 打开二级抽屉
+const act_openSecondaryDrawer = (type) => {
+    // type  1:展厅  2:模板
+    g_secondaryDrawerType.value = type;
+    type == 1 ? get_userScenesDataList() : get_userTemplatesDataList();
+    g_innerDrawer.value = true;
+}
+
+// 选择展厅
+const act_handleCurrentChange = (val) => {
+    if (g_secondaryDrawerType.value == 1) {
+        g_confirmedForm.appName = val?.title
+        g_confirmedForm.appId = val?.id
+    } else {
+        g_confirmedForm.templateName = val?.title
+        g_confirmedForm.templateId = val?.id
+    }
+}
+
+// 确定选择展厅
+const act_ensureGallert = () => {
+    // 关闭抽屉
+    g_innerDrawer.value = false;
+    if (g_secondaryDrawerType.value == 1) {
+        g_form.appId = g_confirmedForm.appId;
+        g_form.appName = g_confirmedForm.appName;
+    } else {
+        g_form.templateName = g_confirmedForm.templateName;
+        g_form.templateId = g_confirmedForm.templateId;
+    }
+}
+
+// 修改场景信息
+const act_updateScene = () => {
+    updateScenes(g_form).then((res) => {
+        g_useMySceneStore.updateFlag = true;
+        g_props.get_userScenesDataList();
+        ElMessage({
+            message: '修改成功',
+            grouping: true,
+            type: 'success',
+        })
+    })
+}
+
+// 新增场景
+const act_addScene = () => {
+    console.log("新增场景", g_form)
+    addScenes(g_form).then((res) => {
+        g_useMySceneStore.updateFlag = true;
+        g_props.get_userScenesDataList();
+        ElMessage({
+            message: '创建成功',
+            grouping: true,
+            type: 'success',
+        })
+    }).catch(err => {
+        console.log(err)
+    })
+}
+
+
+
+/************************************************
+ * 获取数据
+ *
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+// 获取场景详细信息
+const getSceneDetail = () => {
+    getScenesDetail(g_pageType.value).then((res1) => {
+        g_form.title = res1.data.title;
+        g_form.appId = res1.data.appId;
+        g_form.spot = res1.data.spot;
+        g_form.templateId = res1.data.templateId;
+        g_form.cover = res1.data.cover;
+        g_form.type = res1.data.type;
+        g_form.status = res1.data.status;
+
+        // 获取展厅数据
+        getAppsDetail(res1.data.appId).then((res2) => {
+            g_form.appName = res2.data.title;
+        })
+
+        // 获取模板数据
+        getTemplatesDetail(res1.data.templateId).then((res) => {
+            g_form.templateName = res.data.title;
+        })
+
+        g_isLoaded.value = true;
+
+    })
+}
+
+// 获取用户展厅列表
+const get_userScenesDataList = () => {
+    getUserAppsList(g_props.g_SceneDataList.userId).then((res) => {
+        console.log("获取用户展厅列表", g_props.g_SceneDataList.userId, res)
+        g_galleryTableData.value = res.data
+    })
+}
+
+// 获取查询列表
+const get_queryDataList = () => {
+    if (g_secondaryDrawerType.value == 1) {
+        getAppsList(g_queryCondition).then((res) => {
+            g_galleryTableData.value = res.data
+        })
+    } else {
+        getTemplatesList(g_queryCondition).then((res) => {
+            g_TemplateTableData.value = res.data
+        })
+    }
+
+}
+
+// 获取用户已购买模板
+const get_userTemplatesDataList = () => {
+    // 获取模板数据
+    getTemplatesBuyList(g_form.userId).then((res) => {
+        g_TemplateTableData.value = res.data;
+    })
+}
+
+
+</script>
+
+<style scoped>
+.form-custom {
+    flex: 1;
+    display: flex;
+    flex-direction: column
+}
+
+::v-deep .el-input {
+    flex: 1;
+}
+
+::v-deep .el-form-item__content {
+    display: flex;
+    justify-content: center;
+}
+</style>
+
+<style>
+.dialog_class {
+    background-color: rgb(0, 0, 0, 0) !important;
+}
+</style>

+ 219 - 0
src/pages/User/UserComponents/CreateEditPage/CreateEditShowroom.vue

@@ -0,0 +1,219 @@
+<!-- 创建展厅&修改展厅页面 -->
+
+<template>
+    <div>
+        <!-- 表单数据 -->
+        <el-form v-if="g_isLoaded" :model="g_form" label-width="auto">
+
+            <el-form-item label="展厅名称">
+                <el-input v-model="g_form.title" />
+            </el-form-item>
+
+            <el-form-item label="展厅简介">
+                <el-input v-model="g_form.introduction" type="textarea" />
+            </el-form-item>
+
+            <el-form-item label="展厅封面">
+                <ImageUpload ref="imgchildComponentRef"
+                    :URL="(g_form.cover?.indexOf('http') == 0 ? '' : 'https://') + g_form.cover"
+                    :handleUploadSuccess="act_handleUploadSuccess" />
+            </el-form-item>
+
+            <el-form-item label="展厅视频">
+                <VideoUpload ref="videochildComponentRef" :URL="g_form.video"
+                    :handleUploadSuccess="act_handleUploadSuccess" />
+            </el-form-item>
+
+            <el-form-item>
+                <el-button type="primary" @click="act_onSubmit">{{ g_pageType ? "修改" : "创建" }}</el-button>
+            </el-form-item>
+        </el-form>
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted, defineProps } from 'vue'
+import ImageUpload from '@/components/Common/ImageUpload.vue'
+import VideoUpload from '@/components/Common/VideoUpload.vue'
+import { addAppsList, getAppsDetail, updateAppsList } from '@/http/api/pages/Gallery/index'
+import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
+import { useMyShowroomStore } from '@/store/index'
+
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 获取我的展厅
+const g_useMyShowroomStore = useMyShowroomStore();
+
+// 父组件传递过来的函数
+const g_props = defineProps({
+    act_updateVisibility: Function,
+    get_userAppDataList: Function,
+    g_ShowroomDataList: Object,
+})
+
+// 子组件引用
+const imgchildComponentRef = ref(null);
+const videochildComponentRef = ref(null);
+
+// 页面类型 如果有id 则是编辑页面 如果没有id 则是新增页面
+const g_pageType = ref(g_props?.g_ShowroomDataList?.id)
+
+// 表单数据
+const g_form = reactive({
+    id: g_props.g_ShowroomDataList.id,
+    cover: "", // 缩略图
+    icon: "", // 图标
+    introduction: '', // 简介
+    title: '', // 名称
+    userId: 100, // 用户id
+    video: '', // 视频
+    viewCount: 0, // 浏览量
+    userId: g_props.g_ShowroomDataList.userId,
+})
+
+// 加载状态
+const g_isLoaded = ref(false)
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+    if (g_pageType.value) {
+        get_ShowroomDetail()
+    } else {
+        g_isLoaded.value = true;
+    }
+})
+
+
+
+
+/************************************************
+* 事件处理
+*
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+
+// 提交
+const act_onSubmit = async () => {
+    if (!g_form.title) {
+        ElMessage({
+            message: '请输入展厅名称',
+            grouping: true,
+            type: 'warning',
+        });
+        return;
+    }
+
+    let loadingInstance;
+
+    try {
+        await ElMessageBox.confirm(
+            `${g_pageType.value ? "确定修改该展厅吗" : "确定创建该展厅吗"}`,
+            '温馨提示',
+            {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning',
+            }
+        );
+
+        loadingInstance = ElLoading.service({
+            lock: true,
+            text: '上传中,请稍候...',
+            background: 'rgba(0, 0, 0, 0.7)',
+        });
+
+        if (imgchildComponentRef.value && imgchildComponentRef.value.isFileChanged) {
+            await imgchildComponentRef.value.act_uploadFiles();
+        }
+
+        if (videochildComponentRef.value && videochildComponentRef.value.isFileChanged) {
+            await videochildComponentRef.value.act_uploadFiles();
+        }
+
+        g_pageType.value ? act_updateShowroom() : act_addShowroom();
+        g_props.act_updateVisibility();
+
+    } catch (error) {
+        console.log("操作被取消", error);
+    } finally {
+        if (loadingInstance) {
+            loadingInstance.close();
+        }
+    }
+};
+
+// 上传成功回调
+const act_handleUploadSuccess = (res, type) => {
+    type == "img" ? g_form.cover = res.url : g_form.video = res.url
+}
+
+// 修改展厅信息
+const act_updateShowroom = () => {
+    updateAppsList(g_form).then((res) => {
+        g_useMyShowroomStore.updateFlag = true;
+        g_props.get_userAppDataList();
+        ElMessage({
+            message: '修改成功',
+            grouping: true,
+            type: 'success',
+        })
+    })
+}
+
+// 新增展厅
+const act_addShowroom = () => {
+    addAppsList(g_form).then((res) => {
+        g_useMyShowroomStore.updateFlag = true;
+        g_props.get_userAppDataList();
+        ElMessage({
+            message: '创建成功',
+            grouping: true,
+            type: 'success',
+        })
+    }).catch(err => {
+        console.log(err)
+    })
+}
+
+
+/************************************************
+ * 获取数据
+ *
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+// 获取展厅详细信息
+const get_ShowroomDetail = () => {
+    getAppsDetail(g_props.g_ShowroomDataList.id).then(res => {
+        console.log(res)
+        g_form.title = res.data.title;
+        g_form.introduction = res.data.introduction;
+        g_form.cover = res.data.cover;
+        g_form.video = res.data.video;
+        g_form.icon = res.data.icon;
+        g_form.viewCount = res.data.viewCount;
+        g_isLoaded.value = true;
+    })
+}
+
+
+
+</script>
+
+<style scoped>
+::v-deep .el-form-item__content {
+    display: flex;
+    justify-content: center;
+}
+</style>

+ 633 - 0
src/pages/User/UserComponents/CreateEditPage/CreateEditSpot.vue

@@ -0,0 +1,633 @@
+<!-- 展示点页面 -->
+
+<template>
+    <div class="spotBox">
+
+        <!-- 表格数据 -->
+        <TablePaging :data="g_SpotTableData">
+            <template #table>
+                <el-table-column prop="name" label="名称" />
+
+                <el-table-column label="类型">
+                    <template #default="scope">
+                        {{ drawerBank.g_typeMapping[scope.row.type] }}
+                    </template>
+                </el-table-column>
+
+                <el-table-column label="封面">
+                    <template #default="scope">
+                        <el-image class="avatar"
+                            :src="(scope.row.cover?.indexOf('http') == 0 ? '' : 'https://') + scope.row.cover"
+                            fit="cover" />
+                    </template>
+                </el-table-column>
+                <el-table-column prop="dataUrl" label="数据地址" />
+
+                <el-table-column label="操作" width="100">
+                    <template #default="scope">
+                        <el-button type="warning" @click="act_spotOperation(2, scope.row)">编辑</el-button>
+                    </template>
+                </el-table-column>
+            </template>
+        </TablePaging>
+
+
+        <!-- 展示点抽屉 -->
+        <el-drawer v-model="drawerBank.g_innerDrawer" :title="drawerBank.g_drawerName" :append-to-body="true" size="45%"
+            style="min-width: 300px;" modal-class="dialog_class">
+            <el-form :model="g_form" label-width="auto">
+                <el-form-item label="名称">
+                    <el-input v-model="g_form.name" />
+                </el-form-item>
+
+                <el-form-item label="类型">
+                    <el-input v-model="drawerBank.g_typeMapping[g_form.type]" disabled />
+                </el-form-item>
+
+                <el-form-item label="数据源">
+                    <el-input v-model="g_form.dataUrl" disabled />
+                    <el-button type="danger" style="margin-left: 10px;" @click="act_onUnloadDataSource()">卸载</el-button>
+                    <el-button type="primary" style="margin-left: 10px;"
+                        @click="act_onSelectDataSource()">选择</el-button>
+                </el-form-item>
+
+                <div v-if="g_form.type == 1">
+                    <el-form-item label="显示缩略图">
+                        <el-switch v-model="g_form.config.isCover" active-text="是" active-value="1" inactive-text="否"
+                            inactive-value="0" />
+                    </el-form-item>
+                    <el-form-item label="封面" v-show="g_form.config.isCover == 1">
+                        <ImageUpload ref="imgchildComponentRef"
+                            :URL="(g_form.coverUrl?.indexOf('http') == 0 ? '' : 'https://') + g_form.coverUrl"
+                            :handleUploadSuccess="act_handleUploadSuccess" />
+                    </el-form-item>
+                    <el-form-item label="显示文字">
+                        <el-switch v-model="g_form.config.isText" active-text="是" active-value="1" inactive-text="否"
+                            inactive-value="0" />
+                    </el-form-item>
+                    <el-form-item label="文字" v-show="g_form.config.isText == 1">
+                        <el-input v-model="g_form.config.textUrl" />
+                    </el-form-item>
+                </div>
+
+                <div v-if="g_form.type == 2">
+                    <el-form-item label="显示缩略图">
+                        <el-switch v-model="g_form.config.isCover" active-text="是" active-value="1" inactive-text="否"
+                            inactive-value="0" />
+                    </el-form-item>
+                    <el-form-item label="封面" v-if="g_form.config.isCover == 1">
+                        <ImageUpload ref="imgchildComponentRef"
+                            :URL="(g_form.coverUrl?.indexOf('http') == 0 ? '' : 'https://') + g_form.coverUrl"
+                            :handleUploadSuccess="act_handleUploadSuccess" />
+                    </el-form-item>
+                    <el-form-item label="自动播放视频">
+                        <el-switch v-model="g_form.config.isAuto" active-text="是" active-value="1" inactive-text="否"
+                            inactive-value="0" />
+                    </el-form-item>
+                    <el-form-item label="视频打开方式">
+                        <el-switch v-model="g_form.config.videoType" active-text="打开网页" active-value="1"
+                            inactive-text="页面内打开" inactive-value="0" />
+                    </el-form-item>
+                    <el-form-item label="视频">
+                        <VideoUpload ref="videochildComponentRef" :URL="g_form.coverUrl"
+                            :handleUploadSuccess="act_handleUploadSuccess" />
+                    </el-form-item>
+                </div>
+
+                <div v-if="g_form.type == 3">
+                    <el-form-item label="超链接" v-if="g_form.type == 3">
+                        <el-input v-model="g_form.coverUrl" />
+                    </el-form-item>
+                    <el-form-item label="打开方式">
+                        <el-switch v-model="g_form.config.videoType" active-text="打开新网页" active-value="1"
+                            inactive-text="当前页面切换" inactive-value="0" />
+                    </el-form-item>
+                </div>
+
+                <div v-if="g_form.type == 4">
+                    <el-form-item label="图册" v-if="g_form.type == 4">
+                        <div v-for="item in (g_form.coverUrl || '').split(',')">
+                            <ImageUpload ref="imgchildComponentRef"
+                                :URL="(item?.indexOf('http') == 0 ? '' : 'https://') + item"
+                                :handleUploadSuccess="act_handleUploadSuccess" />
+                        </div>
+                    </el-form-item>
+                    <el-form-item label="打开方式">
+                        <el-switch v-model="g_form.config.atlasType" active-text="图册" active-value="1"
+                            inactive-text="轮播" inactive-value="0" />
+                    </el-form-item>
+                </div>
+
+                <div v-if="g_form.type == 4">
+                    <el-form-item label="模型" v-if="g_form.type == 5">
+                        <el-input v-model="g_form.coverUrl" />
+                    </el-form-item>
+                </div>
+
+                <el-form-item>
+                    <el-button type="primary" @click="act_onSubmit">{{ g_pageType ? "修改" : "创建" }}</el-button>
+                </el-form-item>
+            </el-form>
+
+
+            <!-- 选择数据源 -->
+            <el-drawer v-model="drawerBank.g_dataSourceDrawer" :title="drawerBank.g_dataSourceDrawerName"
+                :append-to-body="true" size="40%" style="min-width: 350px;" modal-class="dialog_class">
+                <el-form>
+
+                    <el-form-item>
+                        <TablePaging :data="drawerBank.g_dataSourceDrawerTableData"
+                            :current-change="act_handleCurrentChange">
+                            <template #table>
+                                <el-table-column prop="title" label="名称" />
+                                <el-table-column label="类型">
+                                    <template #default="scope">
+                                        {{ drawerBank.g_typeMapping[String(scope.row.type)] }}
+                                    </template>
+                                </el-table-column>
+                                <el-table-column prop="url" label="地址" />
+                                <el-table-column label="缩略图">
+                                    <template #default="scope">
+                                        <el-image class="avatar"
+                                            :src="(scope.row.cover?.indexOf('http') == 0 ? '' : 'https://') + scope.row.cover"
+                                            fit="cover" />
+                                    </template>
+                                </el-table-column>
+
+                                <el-table-column prop="spot" label="点位" />
+                            </template>
+                        </TablePaging>
+
+                        <!-- <el-table :data="drawerBank.g_dataSourceDrawerTableData" border style="width: 100%"
+                            @current-change="act_handleCurrentChange" empty-text="暂无数据">
+                            <el-table-column prop="title" label="名称" />
+                            <el-table-column label="类型">
+                                <template #default="scope">
+                                    {{ g_dataSourceType[scope.row.type] }}
+                                </template>
+                            </el-table-column>
+                            <el-table-column prop="url" label="地址" />
+
+                            <el-table-column label="缩略图">
+                                <template #default="scope">
+                                    <el-image class="avatar" :src="'http://' + scope.row.cover" fit="cover" />
+                                </template>
+                            </el-table-column>
+
+                            <el-table-column prop="spot" label="点位" />
+                        </el-table> -->
+
+                    </el-form-item>
+
+                    <el-form-item>
+                        <el-button type="primary" @click="act_ensureDataSource">确定</el-button>
+                    </el-form-item>
+                </el-form>
+            </el-drawer>
+
+        </el-drawer>
+
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessageBox, ElMessage, ElLoading } from 'element-plus'
+import TablePaging from '@/components/Common/TablePaging.vue'
+import { getUserScenepot, getDataSourceList, updateSpot, getDataSourceBySpot } from '@/http/api/pages/Spot/index'
+import { uninstallDataSource, mountDataSource } from '@/http/api/pages/DataSources/index'
+import ImageUpload from '@/components/Common/ImageUpload.vue'
+import VideoUpload from '@/components/Common/VideoUpload.vue'
+
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 父组件传递过来的函数
+const g_props = defineProps({
+    act_updateVisibility: Function,
+    get_userScenesDataList: Function,
+    g_SceneDataList: Object,
+})
+
+// 子组件引用
+const imgchildComponentRef = ref(null);
+const videochildComponentRef = ref(null);
+
+const value3 = ref(true)
+// 数据源类型
+const g_dataSourceType = {
+    1: "Unity版本",
+    2: "全景版本"
+}
+// 表格数据
+const g_SpotTableData = ref([])
+
+// 页面类型 如果有id 则是编辑页面 如果没有id 则是新增页面
+const g_pageType = ref(g_props.g_SceneDataList.id)
+
+const drawerBank = reactive({
+    // 展示点抽屉
+    g_innerDrawer: false, // 状态
+    g_drawerType: '1', // 类型
+    g_drawerName: '数据源创建', // 名称
+    // 数据源抽屉
+    g_dataSourceDrawer: false, // 状态
+    g_dataSourceDrawerName: '数据源选择', // 名称
+    g_dataSourceDrawerTableData: [], // 表格数据
+    // 配置映射
+    g_configurationMapping: {
+        isText: `超链接本页面加载`, // 视频类型
+        textUrl: "",
+    },
+    // 类型映射
+    g_typeMapping: {
+        '1': '图片',
+        '2': '视频',
+        '3': '超链接',
+        '4': '文字',
+        '5': '图册',
+        '6': '模型'
+    },
+})
+
+// 编辑、创建数据
+const g_form = reactive({
+    "id": "", // id
+    "name": "", // 名称
+    "type": "", // 类型
+    "coverUrl": "", // 封面url
+    "dataUrl": "", // 数据url
+    "dataSourceName": "", // 数据源名称
+    "config": {} // 配置
+})
+
+// 数据源暂存
+const g_dataSource = reactive({
+    "id": "", // id
+    "dataUrl": "",  // 数据源地址
+    "dataSourceName": "" // 数据源名称
+})
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+    act_onInit();
+})
+
+/************************************************
+* 事件处理
+*
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+// 初始化页面
+const act_onInit = () => {
+    if (g_pageType.value) {
+        get_SceneDetail();
+    }
+}
+
+// 编辑展示点
+const act_spotOperation = (type, row) => {
+    drawerBank.g_drawerName = type == 1 ? "展示点创建" : "展示点编辑";
+    drawerBank.g_drawerType = type;
+    drawerBank.g_innerDrawer = true;
+    // if (row.dataUrl) {
+    // act_getDataSource({ sceneId: g_pageType.value, spotId: row.id }, (dataSource) => act_SpotInit(row, dataSource))
+    // } else {
+    act_SpotInit(row, {});
+    // }
+}
+
+// 初始化抽屉信息
+const act_SpotInit = (row, dataSource) => {
+    console.log(row);
+    const isCreate = drawerBank.g_drawerType == 1
+    g_form.id = isCreate ? "" : row.id
+    g_form.name = isCreate ? "" : row.name
+    g_form.coverUrl = isCreate ? "" : row.coverUrl
+    g_form.dataSourceId = isCreate ? "" : row.dataSourceId
+    g_form.dataUrl = isCreate ? "" : row.dataUrl
+    g_form.dataSourceName = isCreate ? "" : row.dataSourceName
+    g_form.type = isCreate ? "" : row.type
+    g_form.config = isCreate ? "" : JSON.parse(row.config)
+}
+
+// 选择数据源
+const act_onSelectDataSource = () => {
+    drawerBank.g_dataSourceDrawer = true
+    get_DataSource()
+}
+
+const act_getDataSource = ({ sceneId, spotId }, callback) => {
+    getDataSourceBySpot({ sceneId, spotId }).then(res => {
+        if (res.code == 200) {
+            if (callback) callback(res.data);
+        } else {
+            ElMessage({
+                message: res.msg,
+                type: 'error',
+            })
+        }
+    })
+}
+
+// 卸载数据源
+const act_onUnloadDataSource = () => {
+    if (g_form.dataSourceId) {
+        const dataList = {
+            dataSourceId: g_form.dataSourceId,
+            sceneId: g_pageType.value,
+            spotId: g_form.id
+        }
+        uninstallDataSource(dataList).then(res => {
+            if (res.code == 200) {
+                g_form.dataSourceId = "";
+                g_form.coverUrl = "";
+                g_form.dataUrl = "";
+                ElMessage({
+                    message: "卸载成功",
+                    type: 'success',
+                })
+            } else {
+                ElMessage({
+                    message: res.msg,
+                    type: 'error',
+                })
+            }
+        })
+    }
+}
+
+// 挂载数据源
+const act_onMountDataSource = (dataSource) => {
+    console.log("dataSource", dataSource);
+    if (g_form.dataUrl) {
+        // 先卸载
+        uninstallDataSource({
+            dataSourceId: g_form.dataSourceId,
+            sceneId: g_pageType.value,
+            spotId: g_form.id
+        }).then(res => {
+            if (res.code == 200) {
+                g_form.dataSourceId = "";
+                g_form.coverUrl = "";
+                g_form.dataUrl = "";
+                // 后挂载
+                mountDataSource({
+                    dataSourceId: dataSource.id,
+                    sceneId: g_pageType.value,
+                    spotId: g_form.id
+                }).then(res => {
+                    if (res.code == 200) {
+                        g_form.dataSourceId = dataSource.id;
+                        g_form.coverUrl = dataSource.coverUrl;
+                        g_form.dataUrl = dataSource.dataUrl;
+                        get_SceneDetail();
+                        ElMessage({
+                            message: "挂载成功",
+                            type: 'success',
+                        })
+                    } else {
+                        ElMessage({
+                            message: res.msg,
+                            type: 'error',
+                        })
+                    }
+                })
+            } else {
+                ElMessage({
+                    message: res.msg,
+                    type: 'error',
+                })
+            }
+        })
+    } else {
+        mountDataSource({
+            dataSourceId: dataSource.id,
+            sceneId: g_pageType.value,
+            spotId: g_form.id
+        }).then(res => {
+            if (res.code == 200) {
+                g_form.dataSourceId = dataSource.id;
+                g_form.coverUrl = dataSource.coverUrl;
+                g_form.dataUrl = dataSource.dataUrl;
+                get_SceneDetail();
+                ElMessage({
+                    message: "挂载成功",
+                    type: 'success',
+                })
+            } else {
+                ElMessage({
+                    message: res.msg,
+                    type: 'error',
+                })
+            }
+        })
+    }
+}
+
+// 提交
+const act_onSubmit = async () => {
+    console.log('submit!', g_form)
+
+    let loadingInstance;
+
+    try {
+        await ElMessageBox.confirm(
+            `${g_pageType.value ? "确定修改该场景吗" : "确定创建该场景吗"}`,
+            '温馨提示',
+            {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning',
+            }
+        )
+
+        loadingInstance = ElLoading.service({
+            lock: true,
+            text: '上传中,请稍候...',
+            background: 'rgba(0, 0, 0, 0.7)',
+        });
+
+        if (imgchildComponentRef.value && imgchildComponentRef.value.isFileChanged) {
+            await imgchildComponentRef.value.act_uploadFiles();
+        }
+
+        g_pageType.value ? act_updateScene() : act_addScene();
+        g_props.act_updateVisibility();
+    } catch (error) {
+        console.log("操作被取消", error);
+    } finally {
+        if (loadingInstance) {
+            loadingInstance.close();
+        }
+    }
+}
+
+// 上传成功回调
+const act_handleUploadSuccess = (res, type) => {
+    type == "img" ? g_form.coverUrl = res.url : g_form.coverUrl = res.url
+}
+
+// 查询
+const act_inquire = () => {
+    // if (!g_queryCondition.title) {
+    //     return ElMessage({
+    //         message: '请输入查询条件',
+    //         grouping: true,
+    //         type: 'warning',
+    //     })
+    // }
+}
+
+// 切换配置
+const act_handleSelectionChange = (val) => {
+    console.log(val)
+}
+
+// 选择数据源
+const act_handleCurrentChange = (val) => {
+    console.log(val)
+    if (val) {
+        g_dataSource.id = val.id
+        g_dataSource.dataUrl = val.url
+        g_dataSource.dataSourceName = val.title
+        g_dataSource.coverUrl = val.cover
+    }
+}
+
+// 确定选择数据源
+const act_ensureDataSource = () => {
+    ElMessageBox.confirm(
+        '确定选择该数据源吗?',
+        '温馨提示',
+        {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning',
+        }
+    ).then(() => {
+        act_onMountDataSource(JSON.parse(JSON.stringify(g_dataSource)));
+        drawerBank.g_dataSourceDrawer = false;
+        g_dataSource.id = "";
+        g_dataSource.dataUrl = "";
+        g_dataSource.dataSourceName = "";
+        g_dataSource.coverUrl = "";
+    }).catch(() => {
+
+    })
+}
+
+// 修改展示点
+const act_updateScene = () => {
+    updateSpot({
+        sceneId: g_props.g_SceneDataList.id,
+        spot: g_form
+    }).then((res) => {
+        g_props.get_userScenesDataList();
+        ElMessage({
+            message: '修改成功',
+            grouping: true,
+            type: 'success',
+        })
+    })
+}
+
+// 新增场景
+const act_addScene = () => {
+    // addScenes(g_form).then((res) => {
+    //     g_props.get_userScenesDataList();
+    //     ElMessage({
+    //         message: '创建成功',
+    //         grouping: true,
+    //         type: 'success',
+    //     })
+    // }).catch(err => {
+    //     console.log(err)
+    // })
+}
+
+// 格式化配置字段
+const act_formatConfigData = (data) => {
+    const mappingObject = {
+        isCover: "显示缩略图",
+        isAuto: "自动播放视频",
+        videoType: "视频打开方式",
+    }
+    data.forEach((item) => {
+        const config = JSON.parse(item.config)
+        if (config) {
+            item.config = [{
+                label: Object.keys(config)[0],
+                value: Object.values(config)[0],
+            }]
+        }
+    });
+}
+
+
+/************************************************
+ * 获取数据
+ *
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+// 根据用户场景查询展示点列表
+const get_SceneDetail = () => {
+    getUserScenepot(g_pageType.value).then(res => {
+        console.log("展示点", res)
+        if (res.code == 200) {
+            // act_formatConfigData(res.data)
+            g_SpotTableData.value = res.data
+        }
+    })
+}
+
+// 根据展厅id查询数据源列表
+const get_DataSource = () => {
+    getDataSourceList(g_props.g_SceneDataList.appId).then(res => {
+        console.log("数据源", res)
+        drawerBank.g_dataSourceDrawerTableData = res.data
+
+        console.log("数据源", res)
+    })
+}
+
+
+</script>
+
+<style scoped>
+.spotBox {
+    height: 100%
+}
+
+.buttonSet {
+    margin-bottom: 8px
+}
+
+::v-deep .el-input {
+    flex: 1;
+}
+
+::v-deep .el-form-item__content {
+    display: flex;
+    justify-content: center;
+    flex-wrap: wrap;
+    ;
+}
+</style>
+
+<style>
+.dialog_class {
+    background-color: rgb(0, 0, 0, 0) !important;
+}
+</style>

+ 320 - 0
src/pages/User/UserComponents/myFavorite.vue

@@ -0,0 +1,320 @@
+<!-- 我的收藏页面 -->
+<template>
+    <div class="box">
+        <div class="box-contents">
+            <h2>我的收藏</h2>
+
+            <!-- 内容区 -->
+            <template v-if="g_dataList.length > 0">
+                <div class="box-contnet">
+                    <div class="box-content-item" v-for="item in g_dataList" :key="item.id">
+                        <div class="box-content-item-img">
+                            <el-image
+                                :src="(item.template?.imgUrl?.indexOf('http') == 0 ? '' : 'https://') + item.template?.imgUrl"
+                                fit="cover" />
+                        </div>
+
+                        <div class="box-content-item-fun">
+                            <div class="item-fun-top">
+                                <div class="item-fun-top-name">
+                                    名称:{{ item.template?.title }}
+                                </div>
+                                <div class="item-fun-top-time">
+                                    收藏时间:{{ formatDate(item.template?.createTime, 'YYYY-MM-DD HH:mm:ss') }}
+                                </div>
+                            </div>
+                            <div class="item-fun-content">
+                                {{ item.template?.introduction }}
+                            </div>
+                            <div class="item-fun-bottom">
+                                <el-button-group>
+                                    <!-- <el-button type="primary" @click="act_viewDetails(item)">查看详情</el-button> -->
+                                    <el-button type="danger" @click="act_removeFavorite(item)">取消收藏</el-button>
+                                </el-button-group>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </template>
+            <el-empty v-else description="无数据" />
+
+            <!-- 分页栏 -->
+            <!-- <div class="box-foot">
+                <el-pagination v-model:current-page="g_PagingData.currentPage" v-model:page-size="g_PagingData.pageSize"
+                    :page-sizes="[10, 20, 50, 100]" :small="g_PagingData.small" :disabled="g_PagingData.disabled"
+                    :background="g_PagingData.background" layout="total, sizes, prev, pager, next, jumper"
+                    :total="g_PagingData.total" @size-change="act_handleSizeChange"
+                    @current-change="act_handleCurrentChange" />
+            </div> -->
+        </div>
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { listFavorite, removeFavorite } from '@/http/api/pages/Favorite/index'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useUserStore } from "@/store/index";
+
+/************************************************
+ * 全局变量
+ ************************************************/
+const g_userStore = useUserStore();
+const g_userId = g_userStore.userInfo.user.id;
+
+// 列表数据
+const g_dataList = ref([]);
+
+// 配置分页
+const g_PagingData = ref({
+    currentPage: 1,
+    pageSize: 10,
+    total: 0,
+    small: false,
+    background: true,
+    disabled: false,
+});
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+onMounted(() => {
+    get_favoriteList();
+});
+
+/************************************************
+ * 事件处理
+ ************************************************/
+
+// 取消收藏
+const act_removeFavorite = (item) => {
+    ElMessageBox.confirm(
+        '确定取消收藏吗?',
+        '温馨提示',
+        {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning',
+        }
+    ).then(() => {
+        removeFavorite({ userId: g_userId, templateId: item.templateId, status: 0, id: item.id }).then(() => {
+            ElMessage({
+                message: '取消收藏成功',
+                type: 'success',
+            });
+            get_favoriteList();
+        });
+    }).catch(() => { });
+};
+
+// 改变每页条数
+const act_handleSizeChange = (val) => {
+    g_PagingData.value.pageSize = val;
+    get_favoriteList();
+};
+
+// 改变页码
+const act_handleCurrentChange = (val) => {
+    g_PagingData.value.currentPage = val;
+    get_favoriteList();
+};
+
+/************************************************
+ * 获取数据
+ ************************************************/
+const get_favoriteList = async () => {
+    try {
+        const params = {
+            userId: g_userId,
+            page: g_PagingData.value.currentPage,
+            pageSize: g_PagingData.value.pageSize
+        };
+        const res = await listFavorite(params);
+        g_dataList.value = res.data.filter(item => item.template && item.status == 1);
+        g_PagingData.value.total = res.data.length;
+    } catch (error) {
+        console.error('获取收藏列表失败:', error);
+        ElMessage.error('获取收藏列表失败');
+    }
+};
+
+
+function formatDate(date, format) {
+    if (!date) return "";
+    date = new Date(date);
+    const pad = (num, length = 2) => num.toString().padStart(length, '0');
+
+    const year = date.getFullYear();
+    const month = date.getMonth() + 1;
+    const day = date.getDate();
+    const hours = date.getHours();
+    const minutes = date.getMinutes();
+    const seconds = date.getSeconds();
+    const milliseconds = date.getMilliseconds();
+    const isAM = hours < 12;
+
+    const replacements = {
+        'YYYY': pad(year, 4),
+        'YY': pad(year % 100),
+        'MM': pad(month),
+        'M': month,
+        'DD': pad(day),
+        'D': day,
+        'HH': pad(hours),
+        'H': hours,
+        'hh': pad(hours % 12 || 12),
+        'h': hours % 12 || 12,
+        'mm': pad(minutes),
+        'm': minutes,
+        'ss': pad(seconds),
+        's': seconds,
+        'SSS': pad(milliseconds, 3),
+        'A': isAM ? 'AM' : 'PM',
+        'a': isAM ? 'am' : 'pm'
+    };
+
+    return format.replace(/YYYY|YY|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|SSS|A|a/g, match => replacements[match]);
+}
+
+
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    display: flex;
+    padding: 24px;
+    width: 100%;
+    margin: 0;
+}
+
+.box-contents {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    width: 100%;
+}
+
+.box-contents h2 {
+    margin: 0;
+    font-size: 20px;
+    color: #2c3e50;
+    padding-bottom: 16px;
+    border-bottom: 1px solid #eee;
+}
+
+.box-contnet {
+    flex: 1 1 0;
+    width: 100%;
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
+    grid-gap: 20px;
+    align-content: flex-start;
+    overflow: auto;
+}
+
+.box-content-item {
+    height: 280px;
+    padding: 16px;
+    background-color: #fff;
+    display: flex;
+    flex-direction: column;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    transition: all 0.3s ease;
+    border: 1px solid #eee;
+}
+
+.box-content-item:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.box-content-item-img {
+    flex: 6;
+    background-color: #f5f7fa;
+    overflow: hidden;
+    border-radius: 4px;
+}
+
+.box-content-item-img :deep(.el-image) {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+}
+
+.box-content-item-fun {
+    flex: 3;
+    margin-top: 12px;
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+}
+
+.item-fun-top {
+    flex: 1;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.item-fun-top-name {
+    font-weight: 500;
+    color: #2c3e50;
+}
+
+.item-fun-top-time {
+    font-size: 12px;
+    color: #909399;
+}
+
+.item-fun-content {
+    flex: 2;
+    color: #666;
+    font-size: 14px;
+    line-height: 1.5;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    line-clamp: 3;
+    -webkit-box-orient: vertical;
+}
+
+.item-fun-bottom {
+    flex: 1;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    gap: 8px;
+}
+
+.box-foot {
+    width: 100%;
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 20px;
+}
+
+/* 自定义滚动条样式 */
+.box-contnet::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+    background-color: rgba(0, 0, 0, 0.1);
+}
+
+.box-contnet::-webkit-scrollbar-thumb {
+    background-color: #FD571F;
+}
+
+.box-contnet::-webkit-scrollbar-thumb:hover {
+    background-color: #50bfff;
+}
+</style>

+ 439 - 0
src/pages/User/UserComponents/myOrder.vue

@@ -0,0 +1,439 @@
+<!-- 我的订单页面 -->
+
+<template>
+    <div class="box">
+        <div class="box-contents">
+            <h2>我的订单</h2>
+            <!-- 订单状态筛选 -->
+            <div class="filter-section">
+                <el-radio-group v-model="filterStatus" @change="handleStatusChange">
+                    <el-radio-button label="">全部</el-radio-button>
+                    <el-radio-button label="0">待处理</el-radio-button>
+                    <el-radio-button label="1">已发货</el-radio-button>
+                    <el-radio-button label="2">已支付</el-radio-button>
+                    <el-radio-button label="3">已取消</el-radio-button>
+                </el-radio-group>
+            </div>
+
+            <!-- 表格数据 -->
+            <div class="box-content">
+                <el-table :data="g_dataList" border style="width: 100%" empty-text="暂无数据"
+                    :cell-style="{ whiteSpace: 'nowrap' }">
+                    <el-table-column prop="id" label="订单编号" min-width="180" />
+                    <el-table-column prop="name" label="产品" min-width="180" />
+                    <el-table-column prop="money" label="金额" min-width="120">
+                        <template #default="scope">
+                            ¥{{ scope.row.money?.toFixed(2) || '0.00' }}
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="status" label="订单状态" min-width="100">
+                        <template #default="scope">
+                            <el-tag :type="getStatusType(scope.row.status)">
+                                {{ getStatusText(scope.row.status) }}
+                            </el-tag>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="refundStatus" label="退款状态" min-width="100">
+                        <template #default="scope">
+                            <el-tag :type="getRefundStatusType(scope.row.refundStatus)">
+                                {{ getRefundStatusText(scope.row.refundStatus) }}
+                            </el-tag>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="dataTime" label="购买时间" min-width="180" />
+
+                    <!-- 操作 -->
+                    <el-table-column label="操作" min-width="250" fixed="right">
+                        <template #default="scope">
+                            <el-button-group>
+                                <el-button type="primary" size="small" @click="viewOrderDetailClick(scope.row)">
+                                    查看详情
+                                </el-button>
+                                <el-button v-if="scope.row.status === '0'" type="success" size="small"
+                                    @click="payOrderClick(scope.row)">
+                                    立即支付
+                                </el-button>
+                                <el-button v-if="scope.row.status === '0'" type="danger" size="small"
+                                    @click="cancelOrderClick(scope.row)">
+                                    取消订单
+                                </el-button>
+                                <el-button v-if="scope.row.status === '1' && scope.row.refundStatus === '0'"
+                                    type="warning" size="small" @click="refundClick(scope.row)">
+                                    申请退款
+                                </el-button>
+                                <el-button v-if="scope.row.status === '2'" type="warning" size="small"
+                                    @click="invoiceClick(scope.row)">
+                                    开发票
+                                </el-button>
+                            </el-button-group>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </div>
+
+            <div class="box-foot">
+                <el-pagination v-model:current-page="g_PagingData.currentPage" v-model:page-size="g_PagingData.pageSize"
+                    :page-sizes="[10, 20, 50, 100]" :small="g_PagingData.small" :disabled="g_PagingData.disabled"
+                    :background="g_PagingData.background" layout="total, sizes, prev, pager, next, jumper"
+                    :total="g_PagingData.total" @size-change="act_handleSizeChange"
+                    @current-change="act_handleCurrentChange" />
+            </div>
+        </div>
+    </div>
+
+    <!-- 订单详情弹窗 -->
+    <el-dialog v-model="dialogVisible" title="订单详情" width="60%" :close-on-click-modal="false">
+        <div v-if="currentOrder" class="order-detail">
+            <!-- 订单基本信息 -->
+            <div class="detail-section">
+                <h3>订单信息</h3>
+                <el-descriptions :column="2" border>
+                    <el-descriptions-item label="订单编号">{{ currentOrder.id }}</el-descriptions-item>
+                    <el-descriptions-item label="下单时间">{{ currentOrder.dataTime }}</el-descriptions-item>
+                    <el-descriptions-item label="订单状态">
+                        <el-tag :type="getStatusType(currentOrder.status)">
+                            {{ getStatusText(currentOrder.status) }}
+                        </el-tag>
+                    </el-descriptions-item>
+                    <el-descriptions-item label="退款状态">
+                        <el-tag :type="getRefundStatusType(currentOrder.refundStatus)">
+                            {{ getRefundStatusText(currentOrder.refundStatus) }}
+                        </el-tag>
+                    </el-descriptions-item>
+                    <el-descriptions-item label="退款时间">{{ currentOrder.refundDate || '-' }}</el-descriptions-item>
+                </el-descriptions>
+            </div>
+
+            <!-- 订单明细 -->
+            <div class="detail-section">
+                <h3>订单明细</h3>
+                <el-table :data="currentOrder.orderDetails" border style="width: 100%">
+                    <el-table-column prop="templateId" label="模板ID" min-width="100" />
+                    <el-table-column prop="quantity" label="数量" min-width="100" />
+                    <el-table-column prop="price" label="单价" min-width="120">
+                        <template #default="scope">
+                            ¥{{ scope.row.price?.toFixed(2) || '0.00' }}
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="小计" min-width="120">
+                        <template #default="scope">
+                            ¥{{ (scope.row.price * scope.row.quantity)?.toFixed(2) || '0.00' }}
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="createTime" label="创建时间" min-width="180" />
+                </el-table>
+            </div>
+
+            <!-- 订单金额 -->
+            <div class="detail-section">
+                <h3>订单金额</h3>
+                <div class="amount-info">
+                    <div class="amount-item">
+                        <span>商品总额:</span>
+                        <span>¥{{ currentOrder.money?.toFixed(2) || '0.00' }}</span>
+                    </div>
+                    <div class="amount-item">
+                        <span>实付金额:</span>
+                        <span class="total-amount">¥{{ currentOrder.money?.toFixed(2) || '0.00' }}</span>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { listUserOrder, cancelOrder, getOrderDetail } from '@/http/api/pages/Order/index'
+
+const router = useRouter()
+const filterStatus = ref('')
+
+// 订单列表数据
+const g_dataList = ref([])
+
+// 配置分页
+const g_PagingData = ref({
+    currentPage: 1,
+    pageSize: 10,
+    total: 0,
+    small: false,
+    background: true,
+    disabled: false,
+});
+
+// 弹窗控制
+const dialogVisible = ref(false)
+const currentOrder = ref(null)
+
+// 获取订单状态文本
+const getStatusText = (status) => {
+    console.log(status)
+    const statusMap = {
+        0: '待处理',
+        1: '已发货',
+        2: '已支付',
+        3: '已取消'
+    }
+    return statusMap[status] || '未知'
+}
+
+// 获取订单状态类型
+const getStatusType = (status) => {
+    const typeMap = {
+        0: 'warning',
+        1: 'primary',
+        2: 'success',
+        3: 'danger'
+    }
+    return typeMap[status] || 'info'
+}
+
+// 获取退款状态文本
+const getRefundStatusText = (status) => {
+    const statusMap = {
+        0: '未申请',
+        1: '已申请',
+        2: '已处理'
+    }
+    return statusMap[status] || '未知'
+}
+
+// 获取退款状态类型
+const getRefundStatusType = (status) => {
+    const typeMap = {
+        0: 'info',
+        1: 'warning',
+        2: 'success'
+    }
+    return typeMap[status] || 'info'
+}
+
+// 获取订单列表
+const getOrderListData = async () => {
+    try {
+        const response = await listUserOrder()
+        if (response.code === 200 && response.data) {
+            // 处理订单数据
+            g_dataList.value = response.data.map(order => ({
+                id: order.id,
+                name: order.orderDetailsList?.[0]?.templateId ? `模板${order.orderDetailsList[0].templateId}` : '未知商品',
+                money: order.totalAmount,
+                dataTime: order.orderDate,
+                status: order.orderStatus,
+                payStatus: order.orderStatus === 0 ? 0 : 1,
+                refundStatus: order.refundStatus,
+                orderDetails: order.orderDetailsList
+            }))
+            if (filterStatus.value !== "") {
+                g_dataList.value = g_dataList.value.filter(item => item.status == filterStatus.value);
+            }
+            g_PagingData.value.total = g_dataList.value.length
+        } else {
+            ElMessage.error(response.msg || '获取订单列表失败')
+        }
+    } catch (error) {
+        console.error('获取订单列表失败:', error)
+        ElMessage.error('获取订单列表失败')
+    }
+}
+
+// 查看订单详情
+const viewOrderDetailClick = async (order) => {
+    console.log(order)
+    // TODO: 实现获取订单详情API
+    try {
+        const response = await getOrderDetail(order.id)
+        if (response.code === 200 && response.data) {
+            currentOrder.value = {
+                ...order,
+                orderDetails: response.data.orderDetailsList || []
+            }
+            dialogVisible.value = true
+        } else {
+            ElMessage.error(response.msg || '获取订单详情失败')
+        }
+    } catch (error) {
+        console.error('获取订单详情失败:', error)
+        ElMessage.error('获取订单详情失败')
+    }
+}
+
+// 支付订单
+const payOrderClick = (order) => {
+    router.push({
+        name: 'PayOrder',
+        query: { orderId: order.id }
+    })
+}
+
+// 取消订单
+const cancelOrderClick = async (order) => {
+    try {
+        await ElMessageBox.confirm('确定要取消该订单吗?', '提示', {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+        })
+
+        const response = await cancelOrder({ orderId: order.id })
+        if (response.code === 200) {
+            ElMessage.success('订单已取消')
+            getOrderListData()
+        } else {
+            ElMessage.error(response.message || '取消订单失败')
+        }
+    } catch (error) {
+        if (error !== 'cancel') {
+            console.error('取消订单失败:', error)
+            ElMessage.error('取消订单失败')
+        }
+    }
+}
+
+// 开发票
+const invoiceClick = (order) => {
+    // TODO: 实现开发票功能
+    console.log('开发票:', order)
+}
+
+// 添加退款方法
+const refundClick = (order) => {
+    // TODO: 实现退款功能
+    console.log('申请退款:', order)
+}
+
+// 改变每页条数
+const act_handleSizeChange = (val) => {
+    g_PagingData.value.pageSize = val
+    getOrderListData()
+}
+
+// 改变页码
+const act_handleCurrentChange = (val) => {
+    g_PagingData.value.currentPage = val
+    getOrderListData()
+}
+
+// 处理状态筛选变化
+const handleStatusChange = () => {
+    g_PagingData.value.currentPage = 1
+    getOrderListData()
+}
+
+// 挂载完成
+onMounted(() => {
+    getOrderListData()
+})
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    display: flex;
+    padding: 24px;
+    width: 100%;
+    margin: 0;
+}
+
+.box-contents {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    width: 100%;
+}
+
+.box-contents h2 {
+    margin: 0;
+    font-size: 20px;
+    color: #2c3e50;
+    padding-bottom: 16px;
+    border-bottom: 1px solid #eee;
+}
+
+.filter-section {
+    margin-bottom: 20px;
+}
+
+.box-content {
+    flex: 1;
+    width: 100%;
+    overflow: auto;
+}
+
+.box-foot {
+    display: flex;
+    justify-content: flex-end;
+    padding-top: 20px;
+    border-top: 1px solid #ebeef5;
+}
+
+:deep(.el-button-group) {
+    display: flex;
+    gap: 8px;
+}
+
+:deep(.el-table) {
+    width: 100% !important;
+}
+
+:deep(.el-table__body) {
+    width: 100% !important;
+}
+
+:deep(.el-table__row) {
+    width: 100% !important;
+}
+
+:deep(.el-table__cell) {
+    white-space: nowrap;
+}
+
+.order-detail {
+    padding: 20px;
+}
+
+.detail-section {
+    margin-bottom: 30px;
+}
+
+.detail-section h3 {
+    margin-bottom: 15px;
+    font-size: 16px;
+    color: #303133;
+}
+
+.amount-info {
+    padding: 20px;
+    background-color: #f5f7fa;
+    border-radius: 4px;
+}
+
+.amount-item {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 10px;
+    font-size: 14px;
+}
+
+.amount-item:last-child {
+    margin-bottom: 0;
+    font-size: 16px;
+    font-weight: bold;
+}
+
+.total-amount {
+    color: #f56c6c;
+    font-size: 18px;
+}
+
+:deep(.el-dialog__body) {
+    padding: 20px;
+}
+</style>

+ 373 - 0
src/pages/User/UserComponents/myScene.vue

@@ -0,0 +1,373 @@
+<!-- 我的场景页面 -->
+
+
+<template>
+    <div class="box">
+
+        <div class="box-contents">
+            <h2>我的场景</h2>
+            <!-- 按钮组 -->
+            <div class="box-contents-btn">
+                <el-button type="primary" @click="act_createScene">创建场景</el-button>
+                <el-button type="default" @click="get_userScenesDataList">刷新</el-button>
+            </div>
+
+            <!-- 内容区 -->
+            <div class="box-contnet" v-if="g_dataList.length > 0">
+
+                <div class="box-content-item" v-for="item in g_dataList">
+                    <div class="box-content-item-img">
+                        <el-image style="width: 100%;height: 100%;"
+                            :src="(item.cover?.indexOf('http') == 0 ? '' : 'https://') + item.cover" fit="cover" />
+                    </div>
+
+                    <div class="box-content-item-fun">
+                        <div class="item-fun-top">
+                            <div class="item-fun-top-name">
+                                名称:{{ item.title }}
+                            </div>
+                            <div class="item-fun-top-price">
+                                <el-switch v-model="item.status" class="ml-2" inline-prompt
+                                    style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
+                                    active-text="开启" inactive-text="关闭" @click="act_changeState(item)" />
+                            </div>
+                        </div>
+                        <div class="item-fun-content">
+                            {{ item.introduction }}
+                        </div>
+                        <div class="item-fun-bottom">
+                            <el-button-group>
+                                <el-button type="primary" @click="act_showPoint(item)">展示点管理</el-button>
+                                <el-button type="primary" @click="act_modifiedScene(item)">编辑</el-button>
+                                <!-- <el-button type="danger" @click="act_deleteScene(item)">删除</el-button> -->
+                            </el-button-group>
+                        </div>
+                    </div>
+
+                </div>
+            </div>
+            <el-empty v-else description="无数据" />
+
+            <!-- 分页栏 -->
+            <div class="box-foot">
+                <el-pagination v-model:current-page="g_PagingData.currentPage" v-model:page-size="g_PagingData.pageSize"
+                    :page-sizes="[10, 50, 100, 200]" :small="g_PagingData.small" :disabled="g_PagingData.disabled"
+                    :background="g_PagingData.background" layout="total, sizes, prev, pager, next, jumper"
+                    :total="g_PagingData.total" @size-change="act_handleSizeChange"
+                    @current-change="act_handleCurrentChange" />
+            </div>
+        </div>
+
+
+        <!-- 抽屉 -->
+        <drawer :isVisible="g_drawerVisible" :title="g_drawerTitle" :size="'50%'"
+            @update:isVisible="act_updateVisibility" :CloseCallback="act_closeCallback">
+
+            <!-- 内容 -->
+            <template v-slot:content>
+                <component :is="g_pageComponent[g_pageType]" :act_updateVisibility="act_updateVisibility"
+                    :g_SceneDataList="g_SceneDataList" :get_userScenesDataList="get_userScenesDataList" />
+            </template>
+
+        </drawer>
+
+
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { getScenesList, updateScenes } from '@/http/api/pages/Scene/index'
+import drawer from '@/components/Common/drawer.vue'
+import CreateEditScene from './CreateEditPage/CreateEditScene.vue' // 创建/编辑场景组件
+import CreateEditSpot from './CreateEditPage/CreateEditSpot.vue' // 创建/编辑展示点组件
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useUserStore, useMySceneStore } from "@/store/index";
+
+
+
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 获取用户
+const g_userStore = useUserStore();
+// 获取我的场景
+const g_useMySceneStore = useMySceneStore();
+// 用户id
+const g_userId = g_userStore.userInfo.user.id
+
+// 页面组件
+const g_pageComponent = { CreateEditScene, CreateEditSpot }
+// 页面组件当前状态
+const g_pageType = ref('CreateEditScene')
+
+// 抽屉状态
+const g_drawerVisible = ref(false)
+// 抽屉标题
+const g_drawerTitle = ref("")
+// 选择场景详细数据
+const g_SceneDataList = ref({})
+
+// 模拟数据
+const g_dataList = ref([])
+// 配置分页
+const g_PagingData = ref({
+    currentPage: 1, // 当前页码
+    pageSize: 10, // 每页条数
+    total: 0, // 总条数
+    small: false, // 是否使用小型分页样式
+    background: false, // 是否使用背景主题
+    disabled: false, // 是否禁用
+});
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+    get_userScenesDataList();
+})
+
+
+
+/************************************************
+* 事件处理
+*
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+
+// 创建场景
+const act_createScene = () => {
+    g_drawerTitle.value = '场景创建';
+    g_SceneDataList.value = { id: null, userId: g_userId };
+    g_pageType.value = "CreateEditScene"
+    act_updateVisibility(true);
+}
+
+// 查看修改场景
+const act_modifiedScene = (row) => {
+    g_drawerTitle.value = '场景编辑';
+    g_SceneDataList.value = { id: row.id, userId: g_userId };
+    g_pageType.value = "CreateEditScene"
+    act_updateVisibility(true);
+}
+
+// 查看展示点
+const act_showPoint = (row) => {
+    g_drawerTitle.value = '展示点管理';
+    g_SceneDataList.value = { id: row.id, userId: g_userId, appId: row.appId };
+    g_pageType.value = "CreateEditSpot"
+    act_updateVisibility(true);
+}
+
+// 删除场景
+const act_deleteScene = (row) => {
+    ElMessageBox.confirm(
+        '确定删除改场景吗?',
+        '温馨提示',
+        {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning',
+        }
+    ).then(() => {
+        ElMessage({
+            message: '删除成功',
+            grouping: true,
+            type: 'success',
+        })
+    }).catch(() => {
+
+    })
+}
+
+// 改变状态
+const act_changeState = (item) => {
+    const dataList = { ...item, status: item.status == false ? 0 : 1 }
+    updateScenes(dataList).then(res => {
+        // console.log(res)
+    })
+}
+
+// 改变每页条数
+const act_handleSizeChange = (val) => {
+    g_PagingData.value.pageSize = val
+}
+
+// 改变页码
+const act_handleCurrentChange = (val) => {
+    g_PagingData.value.currentPage = val
+}
+
+// 修改抽屉状态
+const act_updateVisibility = (newValue) => {
+    g_drawerVisible.value = newValue
+}
+
+// 关闭回调
+const act_closeCallback = (done) => {
+    ElMessageBox.confirm(
+        '确定关闭吗?',
+        '温馨提示',
+        {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning',
+        }
+    ).then(() => {
+        done();
+    }).catch(() => {
+
+    })
+}
+
+
+/************************************************
+ * 获取数据
+ *
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+// 获取用户场景列表
+const get_userScenesDataList = async () => {
+    // if (g_useMySceneStore.isUpDataList(g_userId)) {
+    const dataList = await (await getScenesList(g_userId)).data
+    dataList.forEach(item => item.status = item.status == 0 ? false : true);
+    g_dataList.value = dataList;
+    // g_useMySceneStore.setMySceneData({ userId: g_userId, dataList: dataList });
+    // } else {
+    // console.log("获取用户场景列表")
+    // g_dataList.value = g_useMySceneStore.mySceneData.dataList;
+    // }
+}
+
+
+
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    display: flex;
+    padding: 24px;
+    width: 100%;
+    margin: 0;
+}
+
+.box-contents {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    width: 100%;
+}
+
+.box-contents h2 {
+    margin: 0;
+    font-size: 20px;
+    color: #2c3e50;
+    padding-bottom: 16px;
+    border-bottom: 1px solid #eee;
+}
+
+.box-contents-btn {
+    width: 100%;
+}
+
+.box-contnet {
+    flex: 1 1 0;
+    width: 100%;
+    margin: 8px 0px;
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
+    grid-gap: 20px;
+    align-content: flex-start;
+    overflow: auto;
+}
+
+/* 自定义滚动条样式 */
+.box-contnet::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+    background-color: rgba(0, 0, 0, 0.1);
+}
+
+.box-contnet::-webkit-scrollbar-thumb {
+    background-color: #FD571F;
+}
+
+.box-contnet::-webkit-scrollbar-thumb:hover {
+    background-color: #50bfff;
+}
+
+.box-content-item {
+    height: 280px;
+    padding: 16px;
+    background-color: rgb(255, 255, 255);
+    display: flex;
+    flex-direction: column;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    transition: all 0.3s ease;
+    border: 1px solid #eee;
+}
+
+.box-content-item:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.box-content-item-img {
+    flex: 6;
+    background-color: cadetblue;
+    overflow: hidden;
+    border-radius: 4px;
+}
+
+.box-content-item-fun {
+    flex: 3;
+    margin-top: 12px;
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+}
+
+.item-fun-top {
+    flex: 1;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.item-fun-content {
+    flex: 2;
+    margin: 8px 0px;
+    color: #666;
+    font-size: 14px;
+    line-height: 1.5;
+}
+
+.item-fun-bottom {
+    flex: 1;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    gap: 8px;
+}
+
+.box-foot {
+    width: 100%;
+    display: flex;
+    justify-content: flex-end;
+}
+</style>

+ 177 - 0
src/pages/User/UserComponents/myShoppingCart.vue

@@ -0,0 +1,177 @@
+<template>
+  <div class="shopping-cart-container">
+    <div class="shopping-cart-wrapper">
+      <h2>购物车</h2>
+      <el-table :data="cartItems" style="width: 100%">
+        <el-table-column label="商品" min-width="300">
+          <template #default="{ row }">
+            <div class="item-info">
+              <el-image :src="(row.imgUrl?.indexOf('http') == 0 ? '' : 'https://') + row.imgUrl"
+                style="width: 60px; height: 60px" fit="cover" />
+              <span class="item-name">{{ row.name }}</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="price" label="单价" min-width="120">
+          <template #default="{ row }">
+            ¥{{ row.price.toFixed(2) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="数量" min-width="120">
+          <template #default="{ row }">
+            <el-input-number v-model="row.quantity" :min="1" :max="row.stock"
+              @change="(value) => updateQuantity(row.id, value)"></el-input-number>
+          </template>
+        </el-table-column>
+        <el-table-column label="总价" min-width="120">
+          <template #default="{ row }">
+            ¥{{ (row.price * row.quantity).toFixed(2) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" min-width="120">
+          <template #default="{ row }">
+            <el-button type="danger" @click="removeItem(row.id)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="cart-summary">
+        <div class="total-price">
+          总价: ¥{{ totalPrice.toFixed(2) }}
+        </div>
+        <div class="total-items">
+          商品总数: {{ totalItems }}
+        </div>
+      </div>
+      <div class="cart-actions">
+        <el-button type="danger" @click="clearCart">清空购物车</el-button>
+        <el-button type="primary" @click="goToCheckout" :disabled="cartItems.length === 0">去结算</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import { useRouter } from 'vue-router';
+import { useShoppingCartStore } from '@/store/modules/shoppingCart';
+import { ElMessage } from 'element-plus';
+
+const router = useRouter();
+const cartStore = useShoppingCartStore();
+
+const cartItems = computed(() => cartStore.cartItems);
+const totalPrice = computed(() => cartStore.totalPrice);
+const totalItems = computed(() => cartStore.totalItems);
+
+const updateQuantity = (itemId, quantity) => {
+  cartStore.updateQuantity(itemId, quantity);
+};
+
+const removeItem = (itemId) => {
+  cartStore.removeItem(itemId);
+  ElMessage.success('商品已从购物车移除');
+};
+
+const clearCart = () => {
+  cartStore.clearCart();
+  ElMessage.success('购物车已清空');
+};
+
+const goToCheckout = () => {
+  if (cartItems.value.length === 0) {
+    ElMessage.warning('购物车为空');
+    return;
+  }
+
+  // 将购物车商品数据传递给确认订单页面
+  router.push({
+    name: 'ConfirmOrder',
+    query: {
+      items: JSON.stringify(cartItems.value.map(item => ({
+        id: item.id,
+        name: item.name,
+        price: item.price,
+        quantity: item.quantity,
+        imgUrl: item.imgUrl
+      })))
+    }
+  });
+};
+</script>
+
+<style scoped>
+.shopping-cart-container {
+  flex: 1;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  display: flex;
+  padding: 24px;
+  width: 100%;
+  margin: 0;
+}
+
+.shopping-cart-wrapper {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  width: 100%;
+}
+
+h2 {
+  margin: 0;
+  font-size: 20px;
+  color: #2c3e50;
+  padding-bottom: 16px;
+  border-bottom: 1px solid #eee;
+}
+
+.item-info {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.item-name {
+  flex: 1;
+  word-break: break-all;
+  white-space: normal;
+}
+
+.cart-summary {
+  margin-top: 20px;
+  padding: 20px;
+  background-color: #f8f9fa;
+  border-radius: 8px;
+}
+
+.total-price {
+  font-size: 18px;
+  font-weight: bold;
+  color: #f56c6c;
+}
+
+.total-items {
+  margin-top: 10px;
+  color: #606266;
+}
+
+.cart-actions {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding-top: 20px;
+  border-top: 1px solid #eee;
+}
+
+:deep(.el-table) {
+  --el-table-border-color: #ebeef5;
+  --el-table-header-bg-color: #f8f9fa;
+}
+
+:deep(.el-input-number) {
+  width: 120px;
+}
+</style>

+ 366 - 0
src/pages/User/UserComponents/myShowroom.vue

@@ -0,0 +1,366 @@
+<!-- 我的展厅页面 -->
+
+<template>
+    <div class="box">
+
+        <div class="box-contents">
+            <h2>我的展厅</h2>
+            <!-- 按钮组 -->
+            <div class="box-contents-btn">
+                <el-button type="primary" @click="act_createShowroom">创建展厅</el-button>
+                <el-button type="default" @click="get_userAppDataList">刷新</el-button>
+            </div>
+
+            <!-- 内容区 -->
+            <div class="box-contnet" v-if="g_dataList.length > 0">
+
+                <div class="box-content-item" v-for="item in g_dataList" :key="item.id">
+                    <div class="box-content-item-img">
+                        <el-image class="avatar"
+                            :src="(item.cover?.indexOf('http') == 0 ? '' : 'https://') + item.cover" fit="cover" />
+                    </div>
+
+                    <div class="box-content-item-fun">
+                        <div class="item-fun-top">
+                            <div class="item-fun-top-name">
+                                名称:{{ item.title }}
+                            </div>
+                            <div class="item-fun-top-price">
+                                <el-switch v-model="item.status" class="ml-2" inline-prompt
+                                    style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
+                                    active-text="开启" inactive-text="关闭" @click="act_changeState(item)" />
+                            </div>
+                        </div>
+                        <div class="item-fun-content">
+                            {{ item.introduction }}
+                        </div>
+                        <div class="item-fun-bottom">
+                            <el-button-group>
+                                <el-button type="primary" @click="act_DataSources(item.id)">数据源管理</el-button>
+                                <el-button type="primary" @click="act_modifiedShowroom(item.id)">编辑</el-button>
+                            </el-button-group>
+                        </div>
+                    </div>
+
+                </div>
+            </div>
+            <el-empty v-else description="无数据" />
+            <!-- 分页栏 -->
+            <div class="box-foot">
+                <el-pagination v-model:current-page="g_PagingData.currentPage" v-model:page-size="g_PagingData.pageSize"
+                    :page-sizes="[10, 50, 100, 200]" :small="g_PagingData.small" :disabled="g_PagingData.disabled"
+                    :background="g_PagingData.background" layout="total, sizes, prev, pager, next, jumper"
+                    :total="g_PagingData.total" @size-change="act_handleSizeChange"
+                    @current-change="act_handleCurrentChange" />
+            </div>
+        </div>
+
+
+        <!-- 抽屉 -->
+        <drawer :isVisible="g_drawerVisible" :title="g_drawerTitle" :size="'50%'"
+            @update:isVisible="act_updateVisibility" :CloseCallback="act_closeCallback">
+
+            <!-- 内容 -->
+            <template v-slot:content>
+                <component :is="g_pageComponent[g_pageType]" :act_updateVisibility="act_updateVisibility"
+                    :g_ShowroomDataList="g_ShowroomDataList" :get_userAppDataList="get_userAppDataList"></component>
+            </template>
+
+        </drawer>
+
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { getUserAppsList, updateAppsList } from '@/http/api/pages/Gallery/index'
+import drawer from '@/components/Common/drawer.vue' // 抽屉
+import CreateEditShowroom from './CreateEditPage/CreateEditShowroom.vue'; // 创建/编辑展厅组件
+import CreateEditDataSources from './CreateEditPage/CreateEditDataSources.vue'; // 创建/编辑数据源组件
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useUserStore, useMyShowroomStore } from "@/store/index";
+
+
+
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 获取用户
+const g_userStore = useUserStore();
+// 获取我的展厅
+const g_useMyShowroomStore = useMyShowroomStore();
+
+// 用户id
+const g_userId = g_userStore.userInfo.user.id
+
+// 页面组件
+const g_pageComponent = { CreateEditShowroom, CreateEditDataSources }
+// 页面组件当前状态
+const g_pageType = ref('CreateEditShowroom')
+
+// 抽屉状态
+const g_drawerVisible = ref(false)
+// 抽屉标题
+const g_drawerTitle = ref("")
+// 选择展厅详细数据
+const g_ShowroomDataList = ref({})
+
+// 表格数据
+const g_dataList = ref([])
+// 配置分页
+const g_PagingData = ref({
+    currentPage: 1, // 当前页码
+    pageSize: 10, // 每页条数
+    total: 0, // 总条数
+    small: false, // 是否使用小型分页样式
+    background: false, // 是否使用背景主题
+    disabled: false, // 是否禁用
+});
+
+
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+    get_userAppDataList();
+    console.log(g_userStore.userInfo.user)
+})
+
+
+
+
+
+
+/************************************************
+* 事件处理
+*
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+
+// 创建展厅
+const act_createShowroom = () => {
+    g_drawerTitle.value = '展厅创建';
+    g_ShowroomDataList.value = { id: null, userId: g_userId };
+    g_pageType.value = 'CreateEditShowroom'
+    act_updateVisibility(true);
+}
+
+// 查看修改展厅
+const act_modifiedShowroom = (id) => {
+    g_drawerTitle.value = '展厅编辑';
+    g_ShowroomDataList.value = { id: id, userId: g_userId };
+    g_pageType.value = 'CreateEditShowroom'
+    act_updateVisibility(true);
+}
+
+// 查看数据源
+const act_DataSources = (id) => {
+    g_drawerTitle.value = '数据源管理';
+    g_ShowroomDataList.value = { id: id, userId: g_userId };
+    g_pageType.value = 'CreateEditDataSources'
+    act_updateVisibility(true);
+}
+
+// 改变状态
+const act_changeState = (item) => {
+    const dataList = { ...item, status: item.status == false ? 0 : 1 }
+    updateAppsList(dataList).then((res) => {
+
+    })
+}
+
+// 改变每页条数
+const act_handleSizeChange = (val) => {
+    g_PagingData.value.pageSize = val
+}
+
+// 改变页码
+const act_handleCurrentChange = (val) => {
+    g_PagingData.value.currentPage = val
+}
+
+// 修改抽屉状态
+const act_updateVisibility = (newValue = false) => {
+    g_drawerVisible.value = newValue
+}
+
+// 关闭回调
+const act_closeCallback = (done) => {
+    ElMessageBox.confirm(
+        '确定关闭吗?',
+        '温馨提示',
+        {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning',
+        }
+    ).then(() => {
+        done();
+    }).catch(() => {
+
+    })
+}
+
+
+/************************************************
+ * 获取数据
+ *
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+// 获取用户展厅列表
+const get_userAppDataList = async () => {
+    // if (g_useMyShowroomStore.isUpDataList(g_userId)) {
+    const dataList = await (await getUserAppsList(g_userId)).data
+    dataList.forEach(item => item.status = item.status == 0 ? false : true);
+    g_dataList.value = dataList;
+    // g_useMyShowroomStore.setMyShowroomData({ userId: g_userId, dataList: dataList });
+    // } else {
+    // g_dataList.value = g_useMyShowroomStore.myShowroomData.dataList;
+    // }
+}
+
+
+
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    display: flex;
+    padding: 24px;
+    width: 100%;
+    margin: 0;
+    overflow: auto;
+}
+
+.box-contents {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    width: 100%;
+}
+
+.box-contents h2 {
+    margin: 0;
+    font-size: 20px;
+    color: #2c3e50;
+    padding-bottom: 16px;
+    border-bottom: 1px solid #eee;
+}
+
+.box-contents-btn {
+    width: 100%;
+}
+
+.box-contnet {
+    flex: 1 1 0;
+    width: 100%;
+    margin: 8px 0px;
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
+    grid-gap: 20px;
+    align-content: flex-start;
+    overflow: auto;
+}
+
+/* 自定义滚动条样式 */
+.box-contnet::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+    background-color: rgba(0, 0, 0, 0.1);
+}
+
+.box-contnet::-webkit-scrollbar-thumb {
+    background-color: #FD571F;
+}
+
+.box-contnet::-webkit-scrollbar-thumb:hover {
+    background-color: #50bfff;
+}
+
+.box-content-item {
+    height: 280px;
+    padding: 16px;
+    background-color: rgb(255, 255, 255);
+    display: flex;
+    flex-direction: column;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    transition: all 0.3s ease;
+    border: 1px solid #eee;
+}
+
+.box-content-item:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.box-content-item-img {
+    flex: 6;
+    background-color: cadetblue;
+    overflow: hidden;
+    border-radius: 4px;
+}
+
+.box-content-item-fun {
+    flex: 3;
+    margin-top: 12px;
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+}
+
+.item-fun-top {
+    flex: 1;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.item-fun-content {
+    flex: 2;
+    margin: 8px 0px;
+    color: #666;
+    font-size: 14px;
+    line-height: 1.5;
+}
+
+.item-fun-bottom {
+    flex: 1;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    gap: 8px;
+}
+
+.box-foot {
+    width: 100%;
+    display: flex;
+    justify-content: flex-end;
+}
+
+::v-deep .PageDrawer {
+    min-width: 800px;
+    width: 100%;
+    max-width: 800px;
+}
+
+::v-deep .el-image {
+    width: 100%;
+    height: 100%;
+}
+</style>

+ 236 - 0
src/pages/User/UserComponents/myTemplate.vue

@@ -0,0 +1,236 @@
+<!-- 我的模板页面 -->
+
+<template>
+    <div class="box">
+        <div class="box-contents">
+            <h2>我的模板</h2>
+            <div class="box-contnet" v-if="g_dataList.length > 0">
+                <div class="box-content-item" v-for="item in g_dataList">
+                    <div class="box-content-item-img">
+                        <el-image style="width: 100%;height: 100%"
+                            :src="(item.imgUrl?.indexOf('http') == 0 ? '' : 'https://') + item.imgUrl" fit="cover" />
+                    </div>
+
+                    <div class="item-content">
+                        <div class="item-title">{{ item.title }}</div>
+                        <el-text class="item-introduction" line-clamp="2">
+                            {{ item.introduction }}
+                        </el-text>
+                    </div>
+                </div>
+            </div>
+            <el-empty v-else description="无数据" />
+            <div class="box-foot">
+                <el-pagination v-model:current-page="g_PagingData.currentPage" v-model:page-size="g_PagingData.pageSize"
+                    :page-sizes="[10, 50, 100, 200]" :small="g_PagingData.small" :disabled="g_PagingData.disabled"
+                    :background="g_PagingData.background" layout="total, sizes, prev, pager, next, jumper"
+                    :total="g_PagingData.total" @size-change="act_handleSizeChange"
+                    @current-change="act_handleCurrentChange" />
+            </div>
+        </div>
+    </div>
+
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { getTemplatesBuyList } from '@/http/api/pages/Template/index'
+import { useUserStore, useMyTemplateStore } from "@/store/index";
+
+
+
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 获取用户
+const g_userStore = useUserStore();
+// 获取模板
+const g_myTemplateStore = useMyTemplateStore();
+
+// 用户id
+const g_userId = g_userStore.userInfo.user.id
+// 模拟数据
+const g_dataList = ref([])
+// 配置分页
+const g_PagingData = ref({
+    currentPage: 1, // 当前页码
+    pageSize: 10, // 每页条数
+    total: 0, // 总条数
+    small: false, // 是否使用小型分页样式
+    background: false, // 是否使用背景主题
+    disabled: false, // 是否禁用
+});
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+    get_TemplatesBuyList();
+})
+
+
+
+
+/************************************************
+* 事件处理
+*
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+// 改变每页条数
+const act_handleSizeChange = (val) => {
+    g_PagingData.value.pageSize = val
+}
+
+// 改变页码
+const act_handleCurrentChange = (val) => {
+    g_PagingData.value.currentPage = val
+}
+
+
+
+
+
+/************************************************
+ * 获取数据
+ *
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+// 获取用户模板数据
+const get_TemplatesBuyList_v1 = async () => {
+    if (g_myTemplateStore.isUpDataList(g_userId)) {
+        const dataList = await (await getTemplatesBuyList(g_userId)).data
+        g_dataList.value = dataList;
+        g_myTemplateStore.setMyTemplateData({ userId: g_userId, dataList: dataList });
+    } else {
+        g_dataList.value = g_myTemplateStore.myTemplateData.dataList;
+    }
+}
+
+const get_TemplatesBuyList = async () => {
+    var response = await getTemplatesBuyList(g_userId);
+    g_dataList.value = response.data;
+}
+
+
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    display: flex;
+    padding: 24px;
+    width: 100%;
+    margin: 0;
+}
+
+.box-contents {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    width: 100%;
+}
+
+.box-contents h2 {
+    margin: 0;
+    font-size: 20px;
+    color: #2c3e50;
+    padding-bottom: 16px;
+    border-bottom: 1px solid #eee;
+}
+
+.box-contnet {
+    flex: 1 1 0;
+    width: 100%;
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
+    align-content: flex-start;
+    grid-gap: 20px;
+    overflow: auto;
+}
+
+/* 自定义滚动条样式 */
+.box-contnet::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+    background-color: rgba(0, 0, 0, 0.1);
+}
+
+.box-contnet::-webkit-scrollbar-thumb {
+    background-color: #FD571F;
+}
+
+.box-contnet::-webkit-scrollbar-thumb:hover {
+    background-color: #50bfff;
+}
+
+.box-content-item {
+    width: calc(100% - 16px);
+    height: 220px;
+    padding: 16px;
+    background-color: rgb(255, 255, 255);
+    display: flex;
+    flex-direction: column;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    transition: all 0.3s ease;
+    border: 1px solid #eee;
+}
+
+.box-content-item:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.box-content-item-img {
+    flex: 7;
+    background-color: cadetblue;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    overflow: hidden;
+    box-shadow: 0px 0px 3px rgb(183 183 183);
+}
+
+.item-content {
+    flex: 3;
+    padding: 4px 0px;
+}
+
+.item-title {
+    font-size: 20px;
+    font-weight: 600;
+    margin-bottom: 4px
+}
+
+.item-introduction {
+    width: 100%;
+    font-size: 15px
+}
+
+.box-content-item-button {
+    margin-top: 12px;
+    text-align: right;
+    display: flex;
+    justify-content: flex-end;
+    gap: 8px;
+}
+
+.box-foot {
+    width: 100%;
+    display: flex;
+    justify-content: flex-end;
+}
+</style>

+ 354 - 0
src/pages/User/UserComponents/notificationCenter.vue

@@ -0,0 +1,354 @@
+<!-- 通知中心页面 -->
+
+<template>
+    <div class="notification-container">
+        <div class="notification-header">
+            <h2>通知中心</h2>
+            <div class="header-actions">
+                <!-- <el-button type="primary" size="small" @click="act_markAllRead">
+                    全部已读
+                </el-button>
+                <el-button type="danger" size="small" @click="act_clearAll">
+                    清空通知
+                </el-button> -->
+            </div>
+        </div>
+
+        <div class="notification-content">
+            <el-empty v-if="!g_dataList.length" description="暂无通知" />
+            <template v-else>
+                <div v-for="item in g_dataList" :key="item.id" class="notification-item"
+                    :class="{ 'is-read': item.isRead }">
+                    <div class="notification-item-header">
+                        <div class="left">
+                            <el-tag :type="item.typeClass" size="small">{{ item.typeText }}</el-tag>
+                            <span class="title">{{ item.title }}</span>
+                        </div>
+                        <div class="right">
+                            <span class="time">{{ item.time }}</span>
+
+                            <el-button type="text" @click="act_deleteNotification(item.id)">
+                                <el-icon>
+                                    <Delete />
+                                </el-icon>
+                            </el-button>
+                        </div>
+                    </div>
+                    <div class="notification-item-content">
+                        {{ item.content }}
+                    </div>
+                    <div class="notification-item-footer" v-if="!item.isRead">
+                        <!-- <el-button type="primary" link @click="act_markRead(item.id)">标记已读</el-button> -->
+                    </div>
+                </div>
+            </template>
+        </div>
+
+        <div class="notification-footer">
+            <el-pagination v-model:current-page="g_currentPage" v-model:page-size="g_pageSize" :total="g_total"
+                :page-sizes="[10, 20, 30, 50]" layout="total, sizes, prev, pager, next"
+                @size-change="act_handleSizeChange" @current-change="act_handleCurrentChange" />
+        </div>
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, onMounted } from 'vue'
+import { Delete } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useUserStore } from "@/store/index";
+import { getNoticeByUserId, getNotice } from '@/http/api/pages/Notice/index'
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+const g_userStore = useUserStore();
+
+// 分页相关
+const g_currentPage = ref(1)
+const g_pageSize = ref(10)
+const g_total = ref(0)
+
+// 模拟数据
+const g_dataList = ref([])
+
+// 通知类型映射
+const g_noticeTypeMap = {
+    1: { text: '系统通知', type: '' },
+    2: { text: '活动通知', type: 'success' },
+    3: { text: '安全提醒', type: 'danger' }
+}
+
+// 通知状态映射
+const g_noticeStateMap = {
+    1: { text: '未读', isRead: false },
+    2: { text: '已读', isRead: true }
+}
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(async () => {
+    await get_notificationList()
+})
+
+/************************************************
+* 事件处理
+*
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+
+// 获取通知列表
+const get_notificationList = async () => {
+    try {
+        const res = await getNoticeByUserId(g_userStore.userInfo.user.id)
+        if (res.code === 200) {
+            // 处理数据,添加类型和状态的映射
+            g_dataList.value = res.data.map(item => ({
+                ...item,
+                typeText: g_noticeTypeMap[item.type]?.text || '未知类型',
+                typeClass: g_noticeTypeMap[item.type]?.type || 'info',
+                isRead: g_noticeStateMap[item.state]?.isRead || false,
+                time: formatTime(item.createTime) // 假设后端返回createTime字段
+            }))
+            g_total.value = res.data.length
+        } else {
+            ElMessage.error(res.msg || '获取通知列表失败')
+        }
+    } catch (error) {
+        console.error('获取通知列表失败:', error)
+        ElMessage.error('获取通知列表失败')
+    }
+}
+
+// 格式化时间
+const formatTime = (time) => {
+    if (!time) return ''
+    const date = new Date(time)
+    return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
+}
+
+// 获取通知类型对应的样式
+const getNotificationType = (type) => {
+    return g_noticeTypeMap[type]?.type || 'info'
+}
+
+// 标记单条通知为已读
+const act_markRead = async (id) => {
+    try {
+        // TODO: 调用API标记已读
+        const notification = g_dataList.value.find(item => item.id === id)
+        if (notification) {
+            notification.state = 2
+            notification.isRead = true
+        }
+        ElMessage.success('已标记为已读')
+    } catch (error) {
+        console.error('标记已读失败:', error)
+        ElMessage.error('标记已读失败')
+    }
+}
+
+// 标记所有通知为已读
+const act_markAllRead = async () => {
+    try {
+        // TODO: 调用API标记所有已读
+        // await api.markAllNotificationsRead()
+
+        // 模拟API调用成功
+        g_dataList.value.forEach(item => {
+            item.isRead = true
+        })
+        ElMessage.success('已全部标记为已读')
+    } catch (error) {
+        console.error('标记全部已读失败:', error)
+        ElMessage.error('标记全部已读失败')
+    }
+}
+
+// 删除单条通知
+const act_deleteNotification = async (id) => {
+    try {
+        await ElMessageBox.confirm('确定要删除这条通知吗?', '提示', {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+        })
+
+        // TODO: 调用API删除通知
+        // await api.deleteNotification(id)
+
+        // 模拟API调用成功
+        g_dataList.value = g_dataList.value.filter(item => item.id !== id)
+        g_total.value--
+        ElMessage.success('删除成功')
+    } catch (error) {
+        if (error !== 'cancel') {
+            console.error('删除通知失败:', error)
+            ElMessage.error('删除通知失败')
+        }
+    }
+}
+
+// 清空所有通知
+const act_clearAll = async () => {
+    try {
+        await ElMessageBox.confirm('确定要清空所有通知吗?此操作不可恢复!', '警告', {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+        })
+
+        // TODO: 调用API清空通知
+        // await api.clearAllNotifications()
+
+        // 模拟API调用成功
+        g_dataList.value = []
+        g_total.value = 0
+        ElMessage.success('已清空所有通知')
+    } catch (error) {
+        if (error !== 'cancel') {
+            console.error('清空通知失败:', error)
+            ElMessage.error('清空通知失败')
+        }
+    }
+}
+
+// 分页大小改变
+const act_handleSizeChange = async (size) => {
+    g_pageSize.value = size
+    g_currentPage.value = 1
+    await get_notificationList()
+}
+
+// 页码改变
+const act_handleCurrentChange = async (page) => {
+    g_currentPage.value = page
+    await get_notificationList()
+}
+</script>
+
+<style scoped>
+.notification-container {
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    padding: 24px;
+    min-height: calc(100vh - 48px);
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    margin: 0;
+}
+
+.notification-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 24px;
+    padding-bottom: 16px;
+    border-bottom: 1px solid #eee;
+}
+
+.notification-header h2 {
+    margin: 0;
+    font-size: 20px;
+    color: #2c3e50;
+}
+
+.header-actions {
+    display: flex;
+    gap: 12px;
+}
+
+.notification-content {
+    flex: 1;
+    overflow-y: auto;
+    width: 100%;
+}
+
+.notification-item {
+    background-color: #f8f9fa;
+    border-radius: 8px;
+    padding: 16px;
+    margin-bottom: 16px;
+    transition: all 0.3s ease;
+    width: 100%;
+    position: relative;
+}
+
+.notification-item:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+}
+
+.notification-item.is-read {
+    background-color: #fff;
+    border: 1px solid #eee;
+}
+
+.notification-item-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 12px;
+}
+
+.notification-item-header .left {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+}
+
+.notification-item-header .right {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+}
+
+.title {
+    font-size: 16px;
+    font-weight: 500;
+    color: #2c3e50;
+}
+
+.time {
+    color: #909399;
+    font-size: 14px;
+}
+
+.notification-item-content {
+    color: #606266;
+    line-height: 1.6;
+    margin: 12px 0;
+    padding-bottom: 32px;
+}
+
+.notification-item-footer {
+    display: flex;
+    justify-content: flex-end;
+    position: absolute;
+    bottom: 16px;
+    right: 56px;
+}
+
+.notification-footer {
+    margin-top: 24px;
+    display: flex;
+    justify-content: center;
+}
+
+:deep(.el-pagination) {
+    justify-content: center;
+}
+
+:deep(.el-tag) {
+    text-transform: none;
+}
+</style>

ファイルの差分が大きいため隠しています
+ 472 - 0
src/pages/User/UserComponents/personalData.vue


+ 240 - 0
src/pages/User/forget.vue

@@ -0,0 +1,240 @@
+<!-- 忘记密码页面 -->
+
+<template>
+    <div class="box">
+
+        <div class="box-content">
+
+            <div class="box-content-title">
+                <h2>忘记密码</h2>
+                <el-steps style="max-width: 600px" :active="g_progressStatus" align-center>
+                    <el-step title="验证身份" />
+                    <el-step title="重置密码" />
+                    <el-step title="完成修改" />
+                </el-steps>
+            </div>
+
+            <!-- 忘记密码框 1 -->
+            <el-form v-if="g_progressStatus == 1" :model="g_formData" label-position="left" label-width="auto"
+                style="max-width: 600px;">
+
+                <el-form-item label="手机号">
+                    <el-input v-model="g_formData.mobileNumber" placeholder="请输入手机号" />
+                </el-form-item>
+
+                <el-form-item label="验证码">
+                    <el-input v-model="g_formData.code" placeholder="请输入验证码" />
+                    <el-button type="primary" class="box-content-item-forget-getCode"
+                        @click="act_getCode">获取验证码</el-button>
+                </el-form-item>
+
+                <el-form-item class="box-content-item-forget">
+                    <el-button type="primary" @click="act_nextStep">下一步</el-button>
+                </el-form-item>
+
+                <div class="box-content-item-footer">
+                    <el-link :underline="false" @click="act_SwitchPage('/login')">返回登录</el-link>
+                </div>
+            </el-form>
+
+            <!-- 忘记密码框 2 -->
+            <el-form v-if="g_progressStatus == 2" :model="g_formData" label-position="left" label-width="auto"
+                style="width: 50%;max-width: 500px;">
+
+                <el-form-item label="新密码">
+                    <el-input v-model="g_formData.mobileNumber" placeholder="请输入新密码" />
+                </el-form-item>
+
+                <el-form-item label="确认新密码">
+                    <el-input v-model="g_formData.code" placeholder="请重新输入新密码" />
+                </el-form-item>
+
+                <el-form-item class="box-content-item-forget">
+                    <el-button type="primary" @click="act_nextStep">下一步</el-button>
+                </el-form-item>
+
+                <div class="box-content-item-footer">
+                    <el-link :underline="false" @click="act_SwitchPage('/login')">返回登录</el-link>
+                </div>
+            </el-form>
+
+            <!-- 忘记密码框 3 -->
+            <el-form v-if="g_progressStatus == 3" :model="g_formData" label-position="left" label-width="auto"
+                style="width: 50%;max-width: 500px;">
+
+                <el-form-item>
+                    <svg t="1715773378816" class="icon" viewBox="0 0 1024 1024" version="1.1"
+                        xmlns="http://www.w3.org/2000/svg" p-id="4274" width="150" height="150">
+                        <path
+                            d="M511.950005 512.049995m-447.956254 0a447.956254 447.956254 0 1 0 895.912508 0 447.956254 447.956254 0 1 0-895.912508 0Z"
+                            fill="#20B759" p-id="4275"></path>
+                        <path
+                            d="M458.95518 649.636559L289.271751 479.95313c-11.698858-11.698858-30.697002-11.698858-42.39586 0s-11.698858 30.697002 0 42.395859l169.683429 169.68343c11.698858 11.698858 30.697002 11.698858 42.39586 0 11.798848-11.598867 11.798848-30.597012 0-42.39586z"
+                            fill="#FFFFFF" p-id="4276"></path>
+                        <path
+                            d="M777.62406 332.267552c-11.698858-11.698858-30.697002-11.698858-42.39586 0L424.158578 643.437164c-11.698858 11.698858-11.698858 30.697002 0 42.39586s30.697002 11.698858 42.39586 0l311.069622-311.069622c11.798848-11.798848 11.798848-30.796992 0-42.49585z"
+                            fill="#FFFFFF" p-id="4277"></path>
+                    </svg>
+                    <h3>恭喜你,你已成功重置密码</h3>
+                </el-form-item>
+
+                <el-form-item class="box-content-item-forget">
+                    <el-button type="primary" @click="act_SwitchPage('/login')">完成</el-button>
+                </el-form-item>
+            </el-form>
+
+        </div>
+
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+
+
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 获取路由对象
+const g_route = useRoute() // 解构出当前路由对象的各种属性
+const g_router = useRouter() // 解构出路由实例的各种方法
+// 进度状态
+const g_progressStatus = ref(1)
+// 表单数据
+const g_formData = reactive({
+    name: '', // 用户名
+    Email: '', // 邮箱
+    password: '', // 密码
+    confirmPassword: '', // 确认密码
+    mobileNumber: '', // 手机号
+    code: '', // 验证码
+})
+
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+
+})
+
+
+
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+
+// 获取验证码
+const act_getCode = () => {
+    console.log("获取验证码")
+}
+
+// 下一步
+const act_nextStep = () => {
+    console.log("下一步")
+    g_progressStatus.value += 1
+    // g_router.push("/")
+}
+
+// 切换页面
+const act_SwitchPage = (page) => {
+    g_router.push(page)
+}
+
+
+
+
+/************************************************
+ * 获取数据
+ * 
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+
+
+
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    width: 100%;
+    min-height: calc(100% - 66px);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.box-content {
+    min-width: 360px;
+    width: 60%;
+    max-width: 900px;
+    height: 70%;
+    max-height: 530px;
+    padding: 16px;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-evenly;
+    align-items: center;
+    border-radius: 8px;
+    overflow: auto;
+    border: 1px solid rgb(223 223 223);
+    box-shadow: 0px 0px 15px rgb(215 215 215);
+}
+
+.box-content-item-footer {
+    text-align: center;
+}
+
+
+.box-content-item-forget-getCode {
+    margin-left: 10px;
+}
+
+.box-content-title {
+    padding: 32px 0;
+    text-align: center;
+}
+
+::v-deep .el-step {
+    margin: 0px 6px;
+}
+
+::v-deep .el-input {
+    flex: 1;
+}
+
+::v-deep .el-form-item__content {
+    display: flex;
+    justify-content: center;
+}
+
+/* 移动端样式 */
+@media screen and (max-width: 700px) {
+    .box-content {
+        min-width: auto;
+        width: auto;
+        max-width: auto;
+        height: auto;
+        max-height: auto;
+        padding: 0;
+        border-radius: 8px;
+        overflow: auto;
+        border: none;
+        box-shadow: 0 0 0 rgb(0, 0, 0, 0);
+    }
+}
+</style>

+ 335 - 0
src/pages/User/login.vue

@@ -0,0 +1,335 @@
+<!-- 登录/注册页面 -->
+
+<template>
+    <div class="box">
+
+        <div class="box-content">
+
+            <!-- 左边框 -->
+            <div class="box-content-left">
+
+                <div class="box-content-text">
+                    <h1>欢迎使用VROOM</h1>
+                    <h3>Welcome to VROOM</h3>
+                </div>
+
+                <!-- 登录框 -->
+                <el-form :model="g_loginForm" label-position="top" label-width="auto" style="max-width: 600px">
+                    <el-form-item label="账号" prop="username">
+                        <el-input v-model="g_loginForm.username" type="text" size="large" auto-complete="off"
+                            placeholder="请输入账号">
+                        </el-input>
+                    </el-form-item>
+
+                    <el-form-item label="密码" prop="password">
+                        <el-input v-model="g_loginForm.password" type="password" size="large" auto-complete="off"
+                            placeholder="请输入密码" @keyup.enter="act_handleLogin">
+                        </el-input>
+                    </el-form-item>
+
+                    <el-form-item label="验证码" prop="code" v-if="g_captchaEnabled">
+                        <el-input v-model="g_loginForm.code" size="large" auto-complete="off" placeholder="请输入验证码"
+                            style="width: 63%" @keyup.enter="act_handleLogin">
+                        </el-input>
+                        <div class="login-code">
+                            <img :src="g_codeUrl" @click="get_Code" class="login-code-img" />
+                        </div>
+                    </el-form-item>
+
+                    <el-form-item>
+                        <el-checkbox v-model="g_loginForm.rememberMe">记住密码</el-checkbox>
+                        <el-link :underline="false" @click="act_SwitchPage('forget')">忘记密码</el-link>
+                    </el-form-item>
+
+                    <el-form-item style="width:100%;">
+                        <el-button :loading="g_loading" size="large" type="primary" style="width:100%;"
+                            @click.prevent="act_handleLogin">
+                            <span v-if="!g_loading">登 录</span>
+                            <span v-else>登 录 中...</span>
+                        </el-button>
+                    </el-form-item>
+
+
+                </el-form>
+
+                <div class="box-content-item-footer">
+                    <el-link :underline="false" @click="act_SwitchPage('/register')">还没有注册,点击此处注册新账号</el-link>
+                </div>
+            </div>
+
+
+            <!-- 右侧背景图 -->
+            <div class="box-content-right">
+
+            </div>
+
+        </div>
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted, inject } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import Cookies from "js-cookie";
+import { encrypt, decrypt } from "@/http/api/general/jsencrypt";
+import { login, getCodeImg } from '@/http/api/pages/User/index'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useUserStore } from "@/store/index";
+
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 用户数据
+const g_userStore = useUserStore();
+// 获取路由对象
+const g_route = useRoute() // 解构出当前路由对象的各种属性
+const g_router = useRouter() // 解构出路由实例的各种方法
+// 记住密码
+const g_rememb = ref(false)
+// 表单数据
+const g_loginForm = ref({
+    username: "",
+    password: "",
+    rememberMe: false,
+    code: "",
+    uuid: ""
+});
+// 验证码
+const g_codeUrl = ref("");
+// 登录状态
+const g_loading = ref(false);
+// 验证码开关
+const g_captchaEnabled = ref(true);
+// 登录状态
+const g_isLogin = inject('act_isLogin');
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+    get_Code();
+    act_getCookie();
+})
+
+
+
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+
+// 切换页面
+const act_SwitchPage = (page) => {
+    g_router.push(page)
+}
+
+// 登录
+const act_handleLogin = () => {
+    if (!act_validate()) return;
+
+    g_loading.value = true;
+
+    // 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
+    let loginData = {};
+
+    if (g_loginForm.value.rememberMe) {
+        loginData = {
+            username: g_loginForm.value.username,
+            password: encrypt(g_loginForm.value.password),
+            rememberMe: g_loginForm.value.rememberMe
+        };
+    } else {
+        loginData = {
+            username: g_loginForm.value.username,
+        };
+    }
+
+
+    login(g_loginForm.value).then(res => {
+        if (res.code == 200) {
+            // 设置用户信息
+            g_userStore.setUserInfo(res.data.user);
+            // 保存res中的token
+            g_userStore.setToken(res.data.token);
+            g_isLogin({ username: 'JohnDoe' });
+            act_SwitchPage('/');
+        } else {
+            ElMessageBox.alert("<font color='red'> " + res.msg + " </font>", "系统提示", {
+                dangerouslyUseHTMLString: true,
+                type: "warning"
+            })
+            g_loading.value = false;
+        }
+    }).catch(() => {
+        g_loading.value = false;
+        // 重新获取验证码
+        if (g_captchaEnabled.value) {
+            get_Code();
+        }
+    });
+}
+
+// 获取数据
+const act_getCookie = () => {
+    const loginData = g_userStore.userInfo.user
+    if (loginData) {
+        g_loginForm.value.username = loginData.username;
+        g_loginForm.value.password = decrypt(loginData.password) || "";
+        g_loginForm.value.rememberMe = loginData.rememberMe;
+    }
+}
+
+// 校验
+const act_validate = () => {
+    if (g_loginForm.value.username == "") {
+        ElMessage.error("请输入用户名");
+        return false;
+    }
+    if (g_loginForm.value.password == "") {
+        ElMessage.error("请输入密码");
+        return false;
+    }
+    if (g_loginForm.value.code == "") {
+        ElMessage.error("请输入验证码");
+        return false;
+    }
+    return true;
+}
+
+
+
+/************************************************
+ * 获取数据
+ * 
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+// 获取验证码
+const get_Code = () => {
+    getCodeImg().then(res => {
+        g_captchaEnabled.value = res.captchaEnabled == undefined ? true : res.captchaEnabled;
+        if (g_captchaEnabled.value) {
+            g_codeUrl.value = "data:image/gif;base64," + res.img;
+            g_loginForm.value.uuid = res.uuid;
+        }
+    });
+}
+
+
+
+
+
+</script>
+<style scoped>
+.box {
+    flex: 1;
+    width: 100%;
+    min-height: calc(100% - 66px);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.box-content {
+    min-width: 360px;
+    width: 60%;
+    max-width: 900px;
+    height: 70%;
+    max-height: 600px;
+    padding: 16px;
+    display: flex;
+    flex-direction: row;
+    border-radius: 8px;
+    overflow: auto;
+    border: 1px solid rgb(223 223 223);
+    box-shadow: 0px 0px 15px rgb(215 215 215);
+}
+
+.box-content-left {
+    flex: 2;
+    min-width: 280px;
+    margin: 0px 16px 0px 0px;
+    padding: 0px 16px 0px 16px;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+    border-right: 2px solid rgb(217 217 217);
+    overflow: auto;
+}
+
+.box-content-right {
+    flex: 3;
+    background-image: url(https://19089915.s21i.faiusr.com/4/ABUIABAEGAAgv539pQYo0_vlpQcw8gM46wI.png.webp);
+    background-repeat: no-repeat;
+    background-position: center center;
+}
+
+.box-content-text h3 {
+    font-weight: 500;
+}
+
+.fun-block {
+    margin-bottom: 18px;
+    display: flex;
+    align-items: center;
+    flex-direction: row;
+    justify-content: space-between;
+}
+
+::v-deep .el-form-item__content {
+    display: flex;
+    justify-content: space-between;
+}
+
+::v-deep .el-input {
+    flex: 1;
+}
+
+.login-code {
+    margin-left: 10px;
+    display: flex;
+    align-items: center;
+}
+
+.box-content-item-footer {
+    text-align: center;
+}
+
+/* 移动端样式 */
+@media screen and (max-width: 700px) {
+    .box-content {
+        min-width: auto;
+        width: auto;
+        max-width: auto;
+        height: auto;
+        max-height: auto;
+        padding: 0;
+        display: flex;
+        flex-direction: row;
+        border-radius: 8px;
+        overflow: auto;
+        border: none;
+        box-shadow: 0 0 0 rgb(0, 0, 0, 0);
+    }
+
+    .box-content-left {
+        margin: 0;
+        border-right: none;
+    }
+
+    .box-content-right {
+        display: none;
+    }
+
+}
+</style>

+ 131 - 0
src/pages/User/personalCenter.vue

@@ -0,0 +1,131 @@
+<!-- 个人中心主页 -->
+
+<template>
+    <div class="personal-center-box">
+
+        <!-- 左侧导航栏 -->
+        <div class="box_left">
+            <el-tabs tab-position="left" v-model="get_activeTabName" class="demo-tabs" @tab-click="act_handleClick">
+                <el-tab-pane label="个人资料" name="personalData"></el-tab-pane>
+                <el-tab-pane label="我的展厅" name="myShowroom"></el-tab-pane>
+                <el-tab-pane label="我的场景" name="myScene"></el-tab-pane>
+                <el-tab-pane label="我的模板" name="myTemplate"></el-tab-pane>
+                <el-tab-pane label="我的收藏" name="myFavorite"></el-tab-pane>
+                <el-tab-pane label="我的购物车" name="myShoppingCart"></el-tab-pane>
+                <el-tab-pane label="我的订单" name="myOrder"></el-tab-pane>
+                <el-tab-pane label="通知中心" name="notificationCenter"></el-tab-pane>
+            </el-tabs>
+        </div>
+
+        <!-- 右侧内容 -->
+        <div class="box_right">
+            <component :is="get_component()"></component>
+        </div>
+
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter, RouterView } from 'vue-router'
+import personalData from './UserComponents/personalData.vue' // 个人资料页面
+import myShowroom from './UserComponents/myShowroom.vue' // 我的展厅
+import myScene from './UserComponents/myScene.vue' // 我的场景
+import myTemplate from './UserComponents/myTemplate.vue' // 我的模板
+import myFavorite from './UserComponents/myFavorite.vue' // 我的收藏
+import myOrder from './UserComponents/myOrder.vue' // 我的订单
+import myShoppingCart from '@/pages/User/UserComponents/myShoppingCart.vue' // 我的购物车
+import notificationCenter from './UserComponents/notificationCenter.vue' // 通知中心
+import { usePersonalCenterStore } from "@/store/index";
+
+
+/************************************************
+ * 全局变量
+ *
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+
+const g_route = useRoute()
+// 组件对象映射
+const g_componentMap = { personalData, myShowroom, myScene, myTemplate, myFavorite, myOrder, notificationCenter, myShoppingCart }
+const g_personalCenterStore = usePersonalCenterStore();
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+
+// 挂载完成
+onMounted(() => {
+    if (g_route.query.tab) {
+        g_personalCenterStore.setActiveTabName(g_route.query.tab);
+    } else {
+        g_personalCenterStore.setActiveTabName('personalData');
+    }
+})
+
+
+
+
+
+/************************************************
+* 事件处理
+*
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+
+// 切换组件
+const act_handleClick = (tab, event) => {
+    g_personalCenterStore.setActiveTabName(tab.props.name);
+}
+
+
+
+
+/************************************************
+ * 获取数据
+ *
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+
+const get_activeTabName = () => {
+    const tabName = g_personalCenterStore.getActiveTabName();
+    return g_personalCenterStore.getActiveTabName();
+}
+
+
+const get_component = () => {
+    return g_componentMap[get_activeTabName()];
+}
+
+
+</script>
+
+<style scoped>
+.personal-center-box {
+    height: calc(100% - 90px);
+    display: flex;
+    padding: 12px 12px;
+}
+
+.box_right {
+    flex: 1;
+    display: flex;
+}
+
+.demo-tabs>.el-tabs__content {
+    padding: 32px;
+    color: #6b778c;
+    font-size: 32px;
+    font-weight: 600;
+}
+
+.el-tabs--right .el-tabs__content,
+.el-tabs--left .el-tabs__content {
+    height: 100%;
+}
+</style>

+ 261 - 0
src/pages/User/register.vue

@@ -0,0 +1,261 @@
+<!-- 注册账号页面 -->
+
+<template>
+    <div class="box">
+
+        <div class="box-content">
+
+            <div class="box-content-title">
+                <h2>填写以下信息完成注册</h2>
+            </div>
+
+            <!-- 注册框 -->
+            <el-form ref="registerRef" :model="g_registerForm" :rules="g_registerRules" label-position="left"
+                label-width="auto" style="max-width: 600px;">
+
+                <el-form-item label="姓名" prop="userName">
+                    <el-input v-model="g_registerForm.userName" type="text" size="large" auto-complete="off"
+                        placeholder="请输入姓名">
+                    </el-input>
+                </el-form-item>
+
+                <el-form-item label="账号" prop="phone">
+                    <el-input v-model="g_registerForm.phone" type="text" size="large" auto-complete="off"
+                        placeholder="请输入手机号">
+                    </el-input>
+                </el-form-item>
+
+                <el-form-item label="密码" prop="password">
+                    <el-input v-model="g_registerForm.password" type="password" size="large" auto-complete="off"
+                        placeholder="请输入密码" @keyup.enter="act_handleRegister">
+                    </el-input>
+                </el-form-item>
+
+                <el-form-item label="确认密码" prop="confirmPassword">
+                    <el-input v-model="g_registerForm.confirmPassword" type="password" size="large" auto-complete="off"
+                        placeholder="请再次输入密码" @keyup.enter="act_handleRegister">
+                    </el-input>
+                </el-form-item>
+
+                <el-form-item label="验证码" prop="code" v-if="g_captchaEnabled">
+                    <el-input size="large" v-model="g_registerForm.code" auto-complete="off" placeholder="验证码"
+                        style="width: 63%" @keyup.enter="act_handleRegister">
+                    </el-input>
+                    <div class="register-code">
+                        <img :src="g_codeUrl" @click="get_Code" class="register-code-img" />
+                    </div>
+                </el-form-item>
+
+                <el-form-item style="width:100%;">
+                    <el-button :loading="g_loading" size="large" type="primary" style="width:100%;"
+                        @click.prevent="act_handleRegister">
+                        <span v-if="!g_loading">注 册</span>
+                        <span v-else>注 册 中...</span>
+                    </el-button>
+                </el-form-item>
+
+                <div class="box-content-item-footer">
+                    <el-link :underline="false" @click="act_SwitchPage('/login')">已有账号,点击这里登录</el-link>
+                </div>
+            </el-form>
+
+        </div>
+
+    </div>
+</template>
+
+<script setup>
+/************************************************
+ *                   引入文件                   *
+ ************************************************/
+import { ref, reactive, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { getCodeImg, register } from '@/http/api/pages/User/index'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+
+
+
+/************************************************
+ * 全局变量
+ * 
+ * 格式: g_ + 事件名     例如: getAppDataList
+ ************************************************/
+// 获取路由对象
+const g_route = useRoute() // 解构出当前路由对象的各种属性
+const g_router = useRouter() // 解构出路由实例的各种方法
+// 表单数据
+const g_registerForm = ref({
+    userName: "",
+    phone: "",
+    password: "",
+    confirmPassword: "",
+    code: "",
+    uuid: ""
+});
+// 验证码
+const act_equalToPassword = (rule, value, callback) => {
+    if (g_registerForm.value.password !== value) {
+        callback(new Error("两次输入的密码不一致"));
+    } else {
+        callback();
+    }
+};
+// 表单验证规则
+const g_registerRules = {
+    userName: [
+        { required: true, trigger: "blur", message: "请输入您的姓名" },
+    ],
+    phone: [
+        { required: true, trigger: "blur", message: "请输入您的账号" },
+        { min: 2, max: 20, message: "用户账号长度必须介于 2 和 20 之间", trigger: "blur" }
+    ],
+    password: [
+        { required: true, trigger: "blur", message: "请输入您的密码" },
+        { min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" }
+    ],
+    confirmPassword: [
+        { required: true, trigger: "blur", message: "请再次输入您的密码" },
+        { required: true, validator: act_equalToPassword, trigger: "blur" }
+    ],
+    code: [{ required: true, trigger: "change", message: "请输入验证码" }]
+};
+// 验证码
+const g_codeUrl = ref("");
+// 登录状态
+const g_loading = ref(false);
+// 验证码开关
+const g_captchaEnabled = ref(true);
+
+
+/************************************************
+ * 生命周期函数
+ ************************************************/
+// 挂载完成
+onMounted(() => {
+    get_Code();
+})
+
+
+
+
+/************************************************
+* 事件处理
+* 
+* 格式: act_ + 事件名     例如: getAppDataList
+************************************************/
+
+// 切换页面
+const act_SwitchPage = (page) => {
+    g_router.push(page)
+}
+
+// 注册
+const act_handleRegister = () => {
+    g_loading.value = true;
+    register({ vrUser: g_registerForm.value }).then(res => {
+        g_loading.value = false;
+        if (res.code == 200) {
+            const phone = g_registerForm.value.phone;
+            ElMessageBox.alert("<font color='red'>恭喜你,您的账号 " + phone + " 注册成功!</font>", "系统提示", {
+                dangerouslyUseHTMLString: true,
+                type: "success",
+            }).then(() => {
+                act_SwitchPage("/login");
+            }).catch(() => {
+                g_captchaEnabled && get_Code();
+            });
+        } else {
+            ElMessageBox.alert("<font color='red'> " + res.msg + " </font>", "错误提示", {
+                dangerouslyUseHTMLString: true,
+                type: "warning"
+            })
+        }
+    })
+}
+
+
+
+/************************************************
+ * 获取数据
+ * 
+ * 格式: get_ + 事件名     例如: get_AppDataList
+ ************************************************/
+
+// 获取验证码
+const get_Code = () => {
+    getCodeImg().then(res => {
+        g_captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled;
+        if (g_captchaEnabled.value) {
+            g_codeUrl.value = "data:image/gif;base64," + res.img;
+            g_registerForm.value.uuid = res.uuid;
+        }
+    });
+}
+
+
+
+</script>
+
+<style scoped>
+.box {
+    flex: 1;
+    width: 100%;
+    min-height: calc(100% - 66px);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.box-content {
+    min-width: 360px;
+    width: 60%;
+    max-width: 900px;
+    height: 70%;
+    max-height: 600px;
+    padding: 16px;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-evenly;
+    align-items: center;
+    border-radius: 8px;
+    overflow: auto;
+    border: 1px solid rgb(223 223 223);
+    box-shadow: 0px 0px 15px rgb(215 215 215);
+}
+
+.box-content-item-footer {
+    text-align: center;
+}
+
+.register-code {
+    margin-left: 10px;
+    display: flex;
+    align-items: center;
+}
+
+::v-deep .el-input {
+    flex: 1;
+}
+
+::v-deep .el-form-item__content {
+    display: flex;
+    justify-content: center;
+}
+
+/* 移动端样式 */
+@media screen and (max-width: 700px) {
+    .box-content {
+        min-width: auto;
+        width: auto;
+        max-width: auto;
+        height: auto;
+        max-height: auto;
+        padding: 16px;
+        border-radius: 8px;
+        overflow: auto;
+        border: none;
+        box-shadow: 0 0 0 rgb(0, 0, 0, 0);
+    }
+}
+</style>

+ 83 - 0
src/router/index.js

@@ -0,0 +1,83 @@
+import {
+  createRouter,
+  createWebHistory,
+  createWebHashHistory,
+} from "vue-router";
+
+const isProduction = import.meta.env.MODE === "production";
+
+const router = createRouter({
+  history: isProduction
+    ? createWebHistory(import.meta.env.BASE_URL)
+    : createWebHashHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: "/",
+      name: "首页",
+      component: () => import("@/pages/Home/index.vue"),
+    },
+    {
+      path: "/showroomCase",
+      name: "展厅案例",
+      component: () => import("@/pages/Gallery/showroomCase.vue"),
+    },
+    {
+      path: "/showroomCaseDetail",
+      name: "展厅案例明细",
+      component: () => import("@/pages/Gallery/showroomCaseDetail.vue"),
+    },
+    {
+      path: "/templateCase",
+      name: "模板案例",
+      component: () => import("@/pages/Template/templateCase.vue"),
+    },
+    {
+      path: "/ArticleColumn",
+      name: "文章栏目",
+      component: () => import("@/pages/Article/ArticleColumn.vue"),
+    },
+    {
+      path: "/personalCenter",
+      name: "个人中心",
+      component: () => import("@/pages/User/personalCenter.vue"),
+    },
+    {
+      path: "/login",
+      name: "登录",
+      component: () => import("@/pages/User/login.vue"),
+    },
+    {
+      path: "/register",
+      name: "注册",
+      component: () => import("@/pages/User/register.vue"),
+    },
+    {
+      path: "/forget",
+      name: "忘记密码",
+      component: () => import("@/pages/User/forget.vue"),
+    },
+    {
+      path: "/ArticleDetails",
+      name: "新闻详情",
+      component: () =>
+        import("@/pages/Article/ArtideContent/ArticleDetails.vue"),
+    },
+    {
+      path: "/ConfirmOrder",
+      name: "ConfirmOrder",
+      component: () => import("@/pages/Order/ConfirmOrder.vue"),
+    },
+    {
+      path: "/PayOrder",
+      name: "PayOrder",
+      component: () => import("@/pages/Order/PayOrder.vue"),
+    },
+    {
+      path: "/OrderSuccess",
+      name: "OrderSuccess",
+      component: () => import("@/pages/Order/OrderSuccess.vue"),
+    },
+  ],
+});
+
+export default router;

+ 20 - 0
src/store/index.js

@@ -0,0 +1,20 @@
+import { createPinia } from "pinia";
+// 引入持久化插件
+import persist from "pinia-plugin-persistedstate";
+
+const pinia = createPinia();
+
+// 使用持久化插件
+pinia.use(persist);
+
+export default pinia;
+
+// 简写
+export * from "./modules/user"; // 用户
+export * from "./modules/templateCase"; // 模板案例
+export * from "./modules/showroomCase"; // 展厅案例
+export * from "./modules/myShowroom"; // 我的展厅
+export * from "./modules/myScene"; // 我的场景
+export * from "./modules/myTemplate"; // 我的模板
+export * from "./modules/personalCenter"; // 个人中心模块
+export * from "./modules/shoppingCart"; // 购物车模块

+ 57 - 0
src/store/modules/myScene.js

@@ -0,0 +1,57 @@
+// 我的场景
+
+import { defineStore } from "pinia";
+import { ref, computed } from "vue";
+import serviceAxios from "@/http/index";
+
+export const useMySceneStore = defineStore(
+  "myScene",
+  () => {
+    // 我的场景
+    const mySceneData = ref({
+      userId: "",
+      dataList: [],
+    });
+
+    // 更新标识
+    const updateFlag = ref(false);
+
+    const doubleCount = computed(() => {});
+
+    // 判断数据是否可更新
+    const isUpDataList = (newUserId) => {
+      console.log("updateFlag", updateFlag.value);
+      if (
+        mySceneData.value.dataList.length == 0 ||
+        mySceneData.value.userId != newUserId ||
+        updateFlag.value == true
+      ) {
+        updateFlag.value = false;
+        return true;
+      }
+      return false;
+    };
+
+    // 设置我的场景
+    const setMySceneData = (dataList) => {
+      mySceneData.value = dataList;
+    };
+
+    // 清除我的场景
+    const clearMySceneData = () => {
+      mySceneData.value = {};
+    };
+
+    return {
+      mySceneData,
+      updateFlag,
+      isUpDataList,
+      setMySceneData,
+      clearMySceneData,
+    };
+  },
+  {
+    // 持久化配置
+    persist: true,
+  }
+);

+ 56 - 0
src/store/modules/myShowroom.js

@@ -0,0 +1,56 @@
+// 我的展厅
+
+import { defineStore } from "pinia";
+import { ref, computed } from "vue";
+import serviceAxios from "@/http/index";
+
+export const useMyShowroomStore = defineStore(
+  "myShowroom",
+  () => {
+    // 我的展厅
+    const myShowroomData = ref({
+      userId: "",
+      dataList: [],
+    });
+
+    // 更新标识
+    const updateFlag = ref(false);
+
+    const doubleCount = computed(() => {});
+
+    // 判断数据是否可更新
+    const isUpDataList = (newUserId) => {
+      if (
+        myShowroomData.value.dataList.length == 0 ||
+        myShowroomData.value.userId != newUserId ||
+        updateFlag.value == true
+      ) {
+        updateFlag.value = false;
+        return true;
+      }
+      return false;
+    };
+
+    // 设置我的展厅
+    const setMyShowroomData = (dataList) => {
+      myShowroomData.value = dataList;
+    };
+
+    // 清除我的展厅
+    const clearMyShowroomData = () => {
+      myShowroomData.value = {};
+    };
+
+    return {
+      myShowroomData,
+      updateFlag,
+      isUpDataList,
+      setMyShowroomData,
+      clearMyShowroomData,
+    };
+  },
+  {
+    // 持久化配置
+    persist: true,
+  }
+);

+ 56 - 0
src/store/modules/myTemplate.js

@@ -0,0 +1,56 @@
+// 我的模板
+
+import { defineStore } from "pinia";
+import { ref, computed } from "vue";
+import serviceAxios from "@/http/index";
+
+export const useMyTemplateStore = defineStore(
+  "myTemplate",
+  () => {
+    // 我的模板
+    const myTemplateData = ref({
+      userId: "",
+      dataList: [],
+    });
+
+    // 更新标识
+    const updateFlag = ref(false);
+
+    const doubleCount = computed(() => {});
+
+    // 判断数据是否可更新
+    const isUpDataList = (newUserId) => {
+      if (
+        myTemplateData.value.dataList.length == 0 ||
+        myTemplateData.value.userId != newUserId ||
+        updateFlag.value == true
+      ) {
+        updateFlag.value = false;
+        return true;
+      }
+      return false;
+    };
+
+    // 设置我的模板
+    const setMyTemplateData = (dataList) => {
+      myTemplateData.value = dataList;
+    };
+
+    // 清除我的模板
+    const clearMyTemplateData = () => {
+      myTemplateData.value = {};
+    };
+
+    return {
+      myTemplateData,
+      updateFlag,
+      isUpDataList,
+      setMyTemplateData,
+      clearMyTemplateData,
+    };
+  },
+  {
+    // 持久化配置
+    persist: true,
+  }
+);

+ 27 - 0
src/store/modules/personalCenter.js

@@ -0,0 +1,27 @@
+// 我的场景
+
+import { defineStore } from "pinia";
+import { ref, computed } from "vue";
+import serviceAxios from "@/http/index";
+
+export const usePersonalCenterStore = defineStore(
+    "personalCenter",
+    () => {
+        const activeTabName = ref("personalData");
+        const setActiveTabName = (payload) => {
+            activeTabName.value = payload;
+        };
+        const getActiveTabName = () => {
+            return activeTabName.value;
+        };
+
+        return {
+            activeTabName,
+            setActiveTabName,
+            getActiveTabName
+        };
+    },
+    {
+        persist: true,
+    }
+);

+ 85 - 0
src/store/modules/shoppingCart.js

@@ -0,0 +1,85 @@
+// 我的场景
+
+import { defineStore } from "pinia";
+import { ref, computed } from "vue";
+
+export const useShoppingCartStore = defineStore(
+  "myShoppingCart",
+  () => {
+    const cartItems = ref([]);
+
+    // 初始化购物车数据
+    const initCart = () => {
+      const savedCart = localStorage.getItem("shoppingCart");
+      if (savedCart) {
+        cartItems.value = JSON.parse(savedCart);
+      }
+    };
+
+    // 添加商品到购物车
+    const addItem = (item) => {
+      const existingItem = cartItems.value.find((i) => i.id === item.id);
+      if (existingItem) {
+        existingItem.quantity += 1;
+      } else {
+        cartItems.value.push({ ...item, quantity: 1 });
+      }
+      saveCart();
+    };
+
+    // 更新商品数量
+    const updateQuantity = (itemId, quantity) => {
+      const item = cartItems.value.find((i) => i.id === itemId);
+      if (item) {
+        item.quantity = quantity;
+        saveCart();
+      }
+    };
+
+    // 删除商品
+    const removeItem = (itemId) => {
+      cartItems.value = cartItems.value.filter((item) => item.id !== itemId);
+      saveCart();
+    };
+
+    // 清空购物车
+    const clearCart = () => {
+      cartItems.value = [];
+      saveCart();
+    };
+
+    // 保存购物车到localStorage
+    const saveCart = () => {
+      localStorage.setItem("shoppingCart", JSON.stringify(cartItems.value));
+    };
+
+    // 计算总价
+    const totalPrice = computed(() => {
+      return cartItems.value.reduce(
+        (sum, item) => sum + item.price * item.quantity,
+        0
+      );
+    });
+
+    // 计算商品总数
+    const totalItems = computed(() => {
+      return cartItems.value.reduce((sum, item) => sum + item.quantity, 0);
+    });
+
+    // 初始化购物车
+    initCart();
+
+    return {
+      cartItems,
+      addItem,
+      updateQuantity,
+      removeItem,
+      clearCart,
+      totalPrice,
+      totalItems,
+    };
+  },
+  {
+    persist: true,
+  }
+);

+ 42 - 0
src/store/modules/showroomCase.js

@@ -0,0 +1,42 @@
+// 展厅案例
+
+import { defineStore } from "pinia";
+import { ref, computed } from "vue";
+import serviceAxios from "@/http/index";
+import { } from "@/http/api/pages/Gallery/index";
+
+export const useShowroomCaseStore = defineStore(
+  "showroomCase",
+  () => {
+    const showroomCaseData = ref([]);
+
+    const doubleCount = computed(() => { });
+
+    /*
+     * 获取展厅案例信息
+     */
+    const getShowroomCaseList = async (data) => {
+      if (showroomCaseData.value.length === 0) {
+        await setShowroomCaseData(data);
+      }
+      return showroomCaseData.value;
+    };
+
+    /*
+     * 设置展厅案例信息
+     */
+    const setShowroomCaseData = async (data) => {
+    };
+
+    return {
+      showroomCaseData,
+      doubleCount,
+      getShowroomCaseList,
+      setShowroomCaseData,
+    };
+  },
+  {
+    // 持久化配置
+    persist: true,
+  }
+);

+ 56 - 0
src/store/modules/templateCase.js

@@ -0,0 +1,56 @@
+// 模板案例
+
+import { defineStore } from "pinia";
+import { ref, computed } from "vue";
+
+export const useTemplateCaseStore = defineStore(
+  "templateCase",
+  () => {
+    const templateCaseList = ref([]);
+
+    /*
+     * 获取模板案例信息
+     */
+    const getTemplatesCaseList = async (
+      { type, title } = {},
+      { isRefetch = false } = {}
+    ) => {
+      var row = templateCaseList.value.find(
+        (item) => item.type == type && item.title == title
+      );
+      if (isRefetch === true) {
+        await setTemplateCaseList({ type, title, row });
+      } else {
+        if (row) {
+          if (!row.data || row.data.length == 0) {
+            await setTemplateCaseList({ type, title, row });
+          }
+        } else {
+          await setTemplateCaseList({ type, title });
+        }
+      }
+      return row
+        ? row.data || []
+        : (
+            templateCaseList.value.find(
+              (item) => item.type == type && item.title == title
+            ) || {}
+          ).data || [];
+    };
+
+    /*
+     * 设置模板案例信息
+     */
+    const setTemplateCaseList = async ({ type, title, row } = {}) => {};
+
+    return {
+      templateCaseList,
+      getTemplatesCaseList,
+      setTemplateCaseList,
+    };
+  },
+  {
+    // 持久化配置
+    persist: true,
+  }
+);

+ 73 - 0
src/store/modules/user.js

@@ -0,0 +1,73 @@
+/*
+ * 全局状态
+ *
+ * 使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾
+ *
+ * 采用选项式语法:
+ * ref() 就是 state 属性
+ * computed() 就是 getters
+ * function() 就是 actions
+ */
+
+import { defineStore } from "pinia";
+import { ref, computed } from "vue";
+import serviceAxios from "@/http/index";
+
+export const useUserStore = defineStore(
+  "user",
+  () => {
+    const userInfo = ref({
+      token: "",
+      user: {}, // 用户信息
+    });
+
+    const doubleCount = computed(() => {});
+
+    /*
+     * 设置用户信息
+     */
+    const setUserInfo = async (payload) => {
+      userInfo.value.user = payload;
+    };
+
+    /*
+     * 清空用户信息
+     */
+    const clearProfile = () => {
+      userInfo.value.user = {};
+      removeToken();
+    };
+
+    /*
+     * 设置token
+     */
+    const setToken = (token) => {
+      userInfo.value.token = token;
+    };
+
+    /*
+     * 移除token
+     */
+    const removeToken = () => {
+      userInfo.value.token = "";
+    };
+
+    // 检查用户是否登录
+    const isUserLogin = () => {
+      return userInfo.value.user && userInfo.value.token;
+    };
+
+    return {
+      userInfo,
+      doubleCount,
+      setUserInfo,
+      clearProfile,
+      setToken,
+      isUserLogin,
+    };
+  },
+  {
+    // 持久化配置
+    persist: true,
+  }
+);

+ 18 - 0
vite.config.js

@@ -0,0 +1,18 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  base: '/',
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url))
+    }
+  },
+ 
+  plugins: [
+    vue()
+  ]
+})