skyblued %!s(int64=2) %!d(string=hai) anos
pai
achega
3e2f29a26a
Modificáronse 100 ficheiros con 3066 adicións e 48 borrados
  1. 6 0
      components.d.ts
  2. 100 17
      package-lock.json
  3. 3 1
      package.json
  4. BIN=BIN
      public/images/___8___________.ai
  5. BIN=BIN
      public/images/_____________.ai
  6. BIN=BIN
      public/images/______________.ai
  7. BIN=BIN
      public/images/______________0.ai
  8. BIN=BIN
      public/images/______________1.ai
  9. BIN=BIN
      public/images/______________2.ai
  10. BIN=BIN
      public/images/______________3.ai
  11. BIN=BIN
      public/images/______________7.ai
  12. BIN=BIN
      public/images/_______________.ai
  13. BIN=BIN
      public/images/_______________4.ai
  14. BIN=BIN
      public/images/_______________5.ai
  15. BIN=BIN
      public/images/_______________6.ai
  16. BIN=BIN
      public/images/________________8.ai
  17. 0 1
      src/App.tsx
  18. 0 0
      src/assets/animate/bigLoad.json
  19. 0 0
      src/assets/animate/kulexiuyunjiaolian.json
  20. BIN=BIN
      src/assets/icon-app.png
  21. BIN=BIN
      src/assets/icon-arrow.png
  22. BIN=BIN
      src/assets/icon-donw.png
  23. BIN=BIN
      src/assets/icon-down.png
  24. BIN=BIN
      src/assets/icon-music-detail.png
  25. BIN=BIN
      src/assets/icon-play.png
  26. BIN=BIN
      src/assets/icon-popup-top.png
  27. BIN=BIN
      src/assets/icon-search-white.png
  28. BIN=BIN
      src/assets/icon-search.png
  29. BIN=BIN
      src/assets/icon-title.png
  30. BIN=BIN
      src/assets/icon-xin.png
  31. BIN=BIN
      src/assets/icon_teacher.png
  32. BIN=BIN
      src/assets/logo-1.png
  33. BIN=BIN
      src/assets/logo-white.png
  34. BIN=BIN
      src/assets/logo.png
  35. BIN=BIN
      src/assets/oStart.png
  36. BIN=BIN
      src/assets/printIcon.png
  37. 0 1
      src/assets/vue.svg
  38. 0 0
      src/components/TheAudio/index.module.less
  39. 47 0
      src/components/TheDown/index.module.less
  40. 99 0
      src/components/TheDown/index.tsx
  41. 106 0
      src/components/TheFooterApp/index.module.less
  42. 91 0
      src/components/TheFooterApp/index.tsx
  43. 30 0
      src/components/TheHeader/index.module.less
  44. 48 0
      src/components/TheHeader/index.tsx
  45. 9 0
      src/components/TheImage/index.module.less
  46. 25 0
      src/components/TheImage/index.tsx
  47. 4 0
      src/components/TheLoading/index.module.less
  48. 15 0
      src/components/TheLoading/index.tsx
  49. 57 0
      src/components/TheMusicGrid/index.module.less
  50. 44 0
      src/components/TheMusicGrid/index.tsx
  51. 9 0
      src/components/TheSearch/index.module.less
  52. 45 0
      src/components/TheSearch/index.tsx
  53. 65 0
      src/components/TheSong/index.module.less
  54. 77 0
      src/components/TheSong/index.tsx
  55. 29 0
      src/components/TheTitle/index.module.less
  56. 35 0
      src/components/TheTitle/index.tsx
  57. 66 0
      src/components/TheVideoGrid/index.module.less
  58. 53 0
      src/components/TheVideoGrid/index.tsx
  59. 24 0
      src/helpers/utils.ts
  60. 2 3
      src/main.ts
  61. 59 11
      src/router/index.ts
  62. 7 1
      src/style.less
  63. 25 0
      src/views/components/banner/index.module.less
  64. 35 0
      src/views/components/banner/index.tsx
  65. 35 0
      src/views/components/boutique-class/index.tsx
  66. 0 0
      src/views/components/hot-album/index.module.less
  67. 54 0
      src/views/components/hot-album/index.tsx
  68. 12 0
      src/views/components/hot-music/index.module.less
  69. 58 0
      src/views/components/hot-music/index.tsx
  70. 61 0
      src/views/down/index.module.less
  71. 46 0
      src/views/down/index.tsx
  72. 2 2
      src/views/home/index.module.less
  73. 18 11
      src/views/home/index.tsx
  74. 30 0
      src/views/home/type.ts
  75. 26 0
      src/views/index/index.module.less
  76. 42 0
      src/views/index/index.tsx
  77. BIN=BIN
      src/views/music/album/images/pan.png
  78. BIN=BIN
      src/views/music/album/images/somePan.png
  79. 86 0
      src/views/music/album/index.module.less
  80. 121 0
      src/views/music/album/index.tsx
  81. 3 0
      src/views/music/index.module.less
  82. 20 0
      src/views/music/index.tsx
  83. 182 0
      src/views/musicDetail/index.module.less
  84. 193 0
      src/views/musicDetail/index.tsx
  85. 66 0
      src/views/search/components/search-list/Album.tsx
  86. 68 0
      src/views/search/components/search-list/Music.tsx
  87. 22 0
      src/views/search/components/search-list/index.module.less
  88. 34 0
      src/views/search/components/search-list/index.tsx
  89. 49 0
      src/views/search/index.module.less
  90. 87 0
      src/views/search/index.tsx
  91. 65 0
      src/views/video/components/video-item/index.module.less
  92. 53 0
      src/views/video/components/video-item/index.tsx
  93. 17 0
      src/views/video/components/video-list/index.module.less
  94. 33 0
      src/views/video/components/video-list/index.tsx
  95. 97 0
      src/views/video/detail/Info.tsx
  96. 135 0
      src/views/video/detail/index.module.less
  97. 136 0
      src/views/video/detail/index.tsx
  98. BIN=BIN
      src/views/video/image/bookIcon.png
  99. BIN=BIN
      src/views/video/image/player.png
  100. BIN=BIN
      src/views/video/image/single.jpg

+ 6 - 0
components.d.ts

@@ -11,3 +11,9 @@ declare module '@vue/runtime-core' {
     RouterView: typeof import('vue-router')['RouterView']
   }
 }
+
+declare module '@vue/runtime-core' {
+  export interface GlobalComponents {
+    LottieAnimation: typeof import('vue3-lottie')['Vue3Lottie']
+  }
+}

+ 100 - 17
package-lock.json

@@ -9,16 +9,17 @@
       "version": "0.0.0",
       "dependencies": {
         "consola": "^2.15.3",
+        "plyr": "^3.7.2",
         "umi-request": "^1.4.0",
         "vant": "^3.5.4",
         "vue": "^3.2.37",
-        "vue-router": "^4.1.3"
+        "vue-router": "^4.1.3",
+        "vue3-lottie": "^2.2.5"
       },
       "devDependencies": {
         "@vitejs/plugin-legacy": "^2.0.1",
         "@vitejs/plugin-vue": "^3.0.3",
         "@vitejs/plugin-vue-jsx": "^2.0.0",
-        "amfe-flexible": "^2.2.1",
         "less": "^4.1.3",
         "less-loader": "^11.0.0",
         "postcss": "^8.4.16",
@@ -1207,12 +1208,6 @@
         "ajv": "^6.9.1"
       }
     },
-    "node_modules/amfe-flexible": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/amfe-flexible/-/amfe-flexible-2.2.1.tgz",
-      "integrity": "sha512-L2VfvDzoETBjhRptg5u/IUuzHSuxm22JpSRb404p/TBGeRfwWmmNEbB+TFPIP/sS/+pbM18bCFH9QnMojLuPNw==",
-      "dev": true
-    },
     "node_modules/ansi-styles": {
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@@ -1510,7 +1505,6 @@
       "version": "3.24.1",
       "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
       "integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==",
-      "dev": true,
       "hasInstallScript": true,
       "funding": {
         "type": "opencollective",
@@ -1522,6 +1516,11 @@
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
       "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
     },
+    "node_modules/custom-event-polyfill": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz",
+      "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w=="
+    },
     "node_modules/debug": {
       "version": "4.3.4",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -2488,6 +2487,11 @@
         "node": ">=6.11.5"
       }
     },
+    "node_modules/loadjs": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/loadjs/-/loadjs-4.2.0.tgz",
+      "integrity": "sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA=="
+    },
     "node_modules/local-pkg": {
       "version": "0.4.2",
       "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz",
@@ -2500,6 +2504,11 @@
         "url": "https://github.com/sponsors/antfu"
       }
     },
+    "node_modules/lottie-web": {
+      "version": "5.9.6",
+      "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.9.6.tgz",
+      "integrity": "sha512-JFs7KsHwflugH5qIXBpB4905yC1Sub2MZWtl/elvO/QC6qj1ApqbUZJyjzJseJUtVpgiDaXQLjBlIJGS7UUUXA=="
+    },
     "node_modules/lower-case": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -2802,6 +2811,18 @@
         "node": ">=6"
       }
     },
+    "node_modules/plyr": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/plyr/-/plyr-3.7.2.tgz",
+      "integrity": "sha512-I0ZC/OI4oJ0iWG9s2rrnO0YFO6aLyrPiQBq9kum0FqITYljwTPBbYL3TZZu8UJQJUq7tUWN18Q7ACwNCkGKABQ==",
+      "dependencies": {
+        "core-js": "^3.22.0",
+        "custom-event-polyfill": "^1.0.7",
+        "loadjs": "^4.2.0",
+        "rangetouch": "^2.0.1",
+        "url-polyfill": "^1.1.12"
+      }
+    },
     "node_modules/postcss": {
       "version": "8.4.16",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
@@ -2895,6 +2916,11 @@
         "safe-buffer": "^5.1.0"
       }
     },
+    "node_modules/rangetouch": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.1.tgz",
+      "integrity": "sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA=="
+    },
     "node_modules/readdirp": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -3395,6 +3421,11 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/url-polyfill": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.12.tgz",
+      "integrity": "sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A=="
+    },
     "node_modules/vant": {
       "version": "3.5.4",
       "resolved": "https://registry.npmjs.org/vant/-/vant-3.5.4.tgz",
@@ -3509,6 +3540,20 @@
         "typescript": "*"
       }
     },
+    "node_modules/vue3-lottie": {
+      "version": "2.2.5",
+      "resolved": "https://registry.npmjs.org/vue3-lottie/-/vue3-lottie-2.2.5.tgz",
+      "integrity": "sha512-r9J5mSJpKjHmwf8OE2sFl3Xi7vRm+F8IDArMBGddLjKxXmFh2PZhdNR85faHt1/MBWLjPjLlKU5VR0N+l1yF8Q==",
+      "dependencies": {
+        "lottie-web": "^5.8.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "vue": "^3.2"
+      }
+    },
     "node_modules/watchpack": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
@@ -4576,12 +4621,6 @@
       "peer": true,
       "requires": {}
     },
-    "amfe-flexible": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/amfe-flexible/-/amfe-flexible-2.2.1.tgz",
-      "integrity": "sha512-L2VfvDzoETBjhRptg5u/IUuzHSuxm22JpSRb404p/TBGeRfwWmmNEbB+TFPIP/sS/+pbM18bCFH9QnMojLuPNw==",
-      "dev": true
-    },
     "ansi-styles": {
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@@ -4811,14 +4850,18 @@
     "core-js": {
       "version": "3.24.1",
       "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
-      "integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==",
-      "dev": true
+      "integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg=="
     },
     "csstype": {
       "version": "2.6.20",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
       "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
     },
+    "custom-event-polyfill": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz",
+      "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w=="
+    },
     "debug": {
       "version": "4.3.4",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -5443,12 +5486,22 @@
       "dev": true,
       "peer": true
     },
+    "loadjs": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/loadjs/-/loadjs-4.2.0.tgz",
+      "integrity": "sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA=="
+    },
     "local-pkg": {
       "version": "0.4.2",
       "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz",
       "integrity": "sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==",
       "dev": true
     },
+    "lottie-web": {
+      "version": "5.9.6",
+      "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.9.6.tgz",
+      "integrity": "sha512-JFs7KsHwflugH5qIXBpB4905yC1Sub2MZWtl/elvO/QC6qj1ApqbUZJyjzJseJUtVpgiDaXQLjBlIJGS7UUUXA=="
+    },
     "lower-case": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -5695,6 +5748,18 @@
       "dev": true,
       "optional": true
     },
+    "plyr": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/plyr/-/plyr-3.7.2.tgz",
+      "integrity": "sha512-I0ZC/OI4oJ0iWG9s2rrnO0YFO6aLyrPiQBq9kum0FqITYljwTPBbYL3TZZu8UJQJUq7tUWN18Q7ACwNCkGKABQ==",
+      "requires": {
+        "core-js": "^3.22.0",
+        "custom-event-polyfill": "^1.0.7",
+        "loadjs": "^4.2.0",
+        "rangetouch": "^2.0.1",
+        "url-polyfill": "^1.1.12"
+      }
+    },
     "postcss": {
       "version": "8.4.16",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
@@ -5750,6 +5815,11 @@
         "safe-buffer": "^5.1.0"
       }
     },
+    "rangetouch": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.1.tgz",
+      "integrity": "sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA=="
+    },
     "readdirp": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -6088,6 +6158,11 @@
         "punycode": "^2.1.0"
       }
     },
+    "url-polyfill": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.12.tgz",
+      "integrity": "sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A=="
+    },
     "vant": {
       "version": "3.5.4",
       "resolved": "https://registry.npmjs.org/vant/-/vant-3.5.4.tgz",
@@ -6156,6 +6231,14 @@
         "@volar/vue-typescript": "0.39.5"
       }
     },
+    "vue3-lottie": {
+      "version": "2.2.5",
+      "resolved": "https://registry.npmjs.org/vue3-lottie/-/vue3-lottie-2.2.5.tgz",
+      "integrity": "sha512-r9J5mSJpKjHmwf8OE2sFl3Xi7vRm+F8IDArMBGddLjKxXmFh2PZhdNR85faHt1/MBWLjPjLlKU5VR0N+l1yF8Q==",
+      "requires": {
+        "lottie-web": "^5.8.1"
+      }
+    },
     "watchpack": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",

+ 3 - 1
package.json

@@ -10,10 +10,12 @@
   },
   "dependencies": {
     "consola": "^2.15.3",
+    "plyr": "^3.7.2",
     "umi-request": "^1.4.0",
     "vant": "^3.5.4",
     "vue": "^3.2.37",
-    "vue-router": "^4.1.3"
+    "vue-router": "^4.1.3",
+    "vue3-lottie": "^2.2.5"
   },
   "devDependencies": {
     "@vitejs/plugin-legacy": "^2.0.1",

BIN=BIN
public/images/___8___________.ai


BIN=BIN
public/images/_____________.ai


BIN=BIN
public/images/______________.ai


BIN=BIN
public/images/______________0.ai


BIN=BIN
public/images/______________1.ai


BIN=BIN
public/images/______________2.ai


BIN=BIN
public/images/______________3.ai


BIN=BIN
public/images/______________7.ai


BIN=BIN
public/images/_______________.ai


BIN=BIN
public/images/_______________4.ai


BIN=BIN
public/images/_______________5.ai


BIN=BIN
public/images/_______________6.ai


BIN=BIN
public/images/________________8.ai


+ 0 - 1
src/App.tsx

@@ -6,7 +6,6 @@ export default defineComponent({
   setup() {
     return () => (
       <>
-        home
         <RouterView />
       </>
     )

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
src/assets/animate/bigLoad.json


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
src/assets/animate/kulexiuyunjiaolian.json


BIN=BIN
src/assets/icon-app.png


BIN=BIN
src/assets/icon-arrow.png


BIN=BIN
src/assets/icon-donw.png


BIN=BIN
src/assets/icon-down.png


BIN=BIN
src/assets/icon-music-detail.png


BIN=BIN
src/assets/icon-play.png


BIN=BIN
src/assets/icon-popup-top.png


BIN=BIN
src/assets/icon-search-white.png


BIN=BIN
src/assets/icon-search.png


BIN=BIN
src/assets/icon-title.png


BIN=BIN
src/assets/icon-xin.png


BIN=BIN
src/assets/icon_teacher.png


BIN=BIN
src/assets/logo-1.png


BIN=BIN
src/assets/logo-white.png


BIN=BIN
src/assets/logo.png


BIN=BIN
src/assets/oStart.png


BIN=BIN
src/assets/printIcon.png


+ 0 - 1
src/assets/vue.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 0 - 0
src/components/TheAudio/index.module.less


+ 47 - 0
src/components/TheDown/index.module.less

@@ -0,0 +1,47 @@
+.downPoup {
+  width: 280px;
+  border-radius: 8px;
+  padding-bottom: 10px;
+}
+.theDown {
+  .title {
+    width: 160px;
+    font-size: 18px;
+    font-weight: 500;
+    color: #333;
+    line-height: 28px;
+    text-align: center;
+  }
+}
+.btns {
+  display: flex;
+  justify-content: center;
+  .btn {
+    padding: 6px 25px;
+    border-radius: 26px;
+    font-size: 14px;
+    font-weight: 500;
+    margin: 3px;
+  }
+  .down {
+    background-color: var(--van-primary-color);
+    color: #fff;
+  }
+  .text {
+    border: 2px solid var(--van-primary-color);
+    color: var(--van-primary-color);
+  }
+}
+.icon {
+  display: block;
+  width: 176px;
+  margin: 20px auto 30px auto;
+}
+.top {
+  display: flex;
+  align-items: center;
+  padding: 20px 10px 0 10px;
+  & > img {
+    width: 64px;
+  }
+}

+ 99 - 0
src/components/TheDown/index.tsx

@@ -0,0 +1,99 @@
+import { defineComponent, ref } from "vue";
+import styles from "./index.module.less";
+import IconTitle from "@/assets/icon-title.png";
+import IconDown from "@/assets/icon-down.png";
+import { Popup } from "vant";
+import { isWeChat } from "@/helpers/utils";
+
+export default defineComponent({
+  name: "TheDown",
+  setup(props, { emit, expose }) {
+    const downLoadApp = () => {
+    //   if (isWeChat()) {
+    //     alert("请点击右上角,使用浏览器打开");
+    //     return;
+    //   }
+
+      // 线上地址
+      // https://itunes.apple.com/cn/app/id1626971149?mt=8   学院
+      // https://itunes.apple.com/cn/app/id1626971695?mt=8   酷乐秀
+      // https://appstore.ks3-cn-beijing.ksyuncs.com/clx-student-domain.apk
+      // https://appstore.ks3-cn-beijing.ksyuncs.com/clx-teacher-domain.apk
+
+      // 酷乐秀 安卓 DEV:
+      // 老师端:https://www.pgyer.com/N2U3https://www.pgyer.com/cooleshow
+      // 学生端:https://www.pgyer.com/70e7https://www.pgyer.com/cooleshow_student
+      // 测试环境
+      // https://www.pgyer.com/powy iOS酷乐秀学生端
+      // https://www.pgyer.com/iO0m iOS 酷乐秀老师端
+      let urlIos = "";
+      let urlAndroid = "";
+      let type = "student";
+      if (location.origin.indexOf("online.colexiu.com") > -1) {
+        if (type === "student") {
+          urlIos = "https://itunes.apple.com/cn/app/id1626971695?mt=8";
+          urlAndroid =
+            "https://appstore.ks3-cn-beijing.ksyuncs.com/clx-student-domain.apk";
+        } else if (type === "teacher") {
+          urlIos = "https://itunes.apple.com/cn/app/id1626971149?mt=8";
+          urlAndroid =
+            "https://appstore.ks3-cn-beijing.ksyuncs.com/clx-teacher-domain.apk";
+        }
+      } else {
+        if (type === "student") {
+          urlIos = "https://www.pgyer.com/powy";
+          urlAndroid = "https://www.pgyer.com/70e7";
+        } else {
+          urlIos = "https://www.pgyer.com/iO0m";
+          urlAndroid = "https://www.pgyer.com/N2U3";
+        }
+      }
+
+      if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
+        window.location.href = urlIos;
+      } else if (/(Android)/i.test(navigator.userAgent)) {
+        window.location.href = urlAndroid;
+      }
+    };
+
+    const show = ref(false);
+    const toggle = () => {
+      show.value = !show.value;
+    };
+
+    expose({
+      downLoadApp,
+      toggle,
+    });
+
+    return () => (
+      <div>
+        <Popup class={styles.downPoup} v-model:show={show.value}>
+          <div class={styles.theDown}>
+            <div class={styles.top}>
+              <img src={IconTitle} alt="" />
+              <span class={styles.title}>下载酷乐秀APP 发现更大的世界</span>
+              <img
+                style={{ transform: "rotateY(180deg)" }}
+                src={IconTitle}
+                alt=""
+              />
+            </div>
+            <img class={styles.icon} src={IconDown} />
+            <div class={styles.btns}>
+              <div class={[styles.btn, styles.down]} onClick={downLoadApp}>
+                下载APP
+              </div>
+              <div
+                class={[styles.btn, styles.text]}
+                onClick={() => (show.value = false)}
+              >
+                继续浏览
+              </div>
+            </div>
+          </div>
+        </Popup>
+      </div>
+    );
+  },
+});

+ 106 - 0
src/components/TheFooterApp/index.module.less

@@ -0,0 +1,106 @@
+.theFooterApp {
+  position: fixed;
+  left: 50%;
+  bottom: 40px;
+  width: 240px;
+  height: 44px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 16px;
+  font-weight: bold;
+  color: #fff;
+  background-color: var(--van-primary-color);
+  border-radius: 20px;
+  transform: translateX(-50%);
+  overflow: hidden;
+  z-index: 100;
+  & > img {
+    width: 25px;
+    height: 25px;
+    margin-right: 12px;
+  }
+  &:active {
+    &::before {
+      opacity: 0.1;
+    }
+  }
+  &::before {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: 100%;
+    height: 100%;
+    background: var(--van-black);
+    border: inherit;
+    border-color: var(--van-black);
+    border-radius: inherit;
+    transform: translate(-50%, -50%);
+    opacity: 0;
+    content: " ";
+  }
+}
+.appContent {
+  background: #f8f8f8;
+  .top {
+    display: flex;
+    justify-content: space-between;
+    padding: 16px;
+    background-image: url("../../assets/icon-popup-top.png");
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    font-size: 16px;
+    font-weight: bold;
+    line-height: 22px;
+    .des {
+      display: flex;
+      align-items: center;
+      font-size: 12px;
+      color: #999;
+    }
+  }
+  .content {
+    padding: 0 16px 6px 16px;
+    .item {
+      display: flex;
+      align-items: center;
+      padding: 12px 16px;
+      background: #ffffff;
+      box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
+      border-radius: 8px;
+      margin-bottom: 12px;
+      :global {
+        .van-button {
+          margin-left: auto;
+          width: 68px;
+          height: 34px;
+        }
+      }
+    }
+    .tag {
+      font-size: 12px;
+      color: #fd6223;
+      background: #ffeae2;
+      border-radius: 4px;
+      padding: 1px 2px;
+      margin-left: 10px;
+    }
+    .description {
+      font-size: 16px;
+      .title {
+        line-height: 24px;
+      }
+      .des {
+        font-size: 12px;
+        color: #999;
+      }
+    }
+    .itemLogo {
+      width: 60px;
+      height: 60px;
+      margin-right: 14px;
+      border: 1px solid #dedede;
+      border-radius: 14px;
+    }
+  }
+}

+ 91 - 0
src/components/TheFooterApp/index.tsx

@@ -0,0 +1,91 @@
+import { defineComponent } from "vue";
+import styles from "./index.module.less";
+import IconLogo1 from "../../assets/logo-1.png";
+import { Button, Icon, Popup } from "vant";
+import { useToggle } from "@vant/use";
+
+export default defineComponent({
+  name: "TheFooterApp",
+  setup() {
+    const [show, toggle] = useToggle(false);
+    const btns = [
+      {
+        title: "酷乐秀",
+        des: "学习海量优质乐谱",
+        tag: "推荐",
+        type: "ColexiuStudent",
+      },
+      {
+        title: "酷乐秀学院",
+        des: "我要提供优质内容",
+        tag: "",
+        type: "ColexiuTeacher",
+      },
+    ];
+    const onTansfer = (type: any) => {
+      // ColexiuStudent 唤起学生端
+      // ColexiuTeacher 唤起老师端
+      const query = {
+        url: "",
+        action: "h5", // app, h5
+        pageTag: 1, // 页面标识
+      };
+      const iosStr = encodeURIComponent(JSON.stringify(query));
+      if (type == "ColexiuStudent") {
+        if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
+          window.location.href = `ColexiuStudent://linkUrl=${iosStr}`;
+        } else if (/(Android)/i.test(navigator.userAgent)) {
+          window.location.href = `colexiustudent://html:8888/SplashActivity?url=${iosStr}`;
+        }
+      } else {
+        if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
+          window.location.href = `ColexiuTeacher://linkUrl=${iosStr}`;
+        } else if (/(Android)/i.test(navigator.userAgent)) {
+          window.location.href = `colexiuteacher://html:8888/SplashActivity?url=${iosStr}`;
+        }
+      }
+    };
+    return () => (
+      <>
+        <div class={styles.theFooterApp} onClick={() => toggle(true)}>
+          <img class={styles.img} src={IconLogo1} />
+          <span>打开APP看海量热门乐谱</span>
+        </div>
+
+        <Popup position="bottom" round v-model:show={show.value}>
+          <div class={styles.appContent}>
+            <div class={styles.top}>
+              <span>打开方式</span>
+              <div class={styles.des} onClick={() => toggle(false)}>
+                <span>继续使用浏览器</span>
+                <Icon name="play" size={10} />
+              </div>
+            </div>
+
+            <div class={styles.content}>
+              {btns.map((n) => (
+                <div class={styles.item}>
+                  <img class={styles.itemLogo} src={IconLogo1} alt="" />
+                  <div class={styles.description}>
+                    <div class={styles.title}>
+                      {n.title}
+                      {n.tag && <span class={styles.tag}>{n.tag}</span>}
+                    </div>
+                    <div class={styles.des}>{n.des}</div>
+                  </div>
+                  <Button
+                    round
+                    type="primary"
+                    onClick={() => onTansfer(n.type)}
+                  >
+                    打开
+                  </Button>
+                </div>
+              ))}
+            </div>
+          </div>
+        </Popup>
+      </>
+    );
+  },
+});

+ 30 - 0
src/components/TheHeader/index.module.less

@@ -0,0 +1,30 @@
+.theHeader {
+  display: flex;
+  align-items: center;
+  height: 55px;
+  padding: 0 18px;
+  border-bottom: 1px solid #f8f8f8;
+  .logo {
+    height: 54%;
+    position: relative;
+    max-width: 60%;
+    & > img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+  .undis {
+    position: absolute;
+    left: 0;
+    clip: rect(0 0 0 0);
+  }
+  .btn {
+    width: 70px;
+    height: 26px;
+    margin-left: auto;
+    margin-right: 14px;
+    font-size: 12px;
+    font-weight: 500;
+    white-space: nowrap;
+  }
+}

+ 48 - 0
src/components/TheHeader/index.tsx

@@ -0,0 +1,48 @@
+import { defineComponent, PropType, ref } from "vue";
+import logo from "@/assets/logo.png";
+import logoDark from "@/assets/logo-white.png";
+import IconSearch from "@/assets/icon-search.png";
+import IconSearchWhite from "@/assets/icon-search-white.png";
+import styles from "./index.module.less";
+import { Button, Icon, Image } from "vant";
+import { useRoute, useRouter } from "vue-router";
+import TheDown from "../TheDown";
+
+export default defineComponent({
+  name: "TheHeader",
+  props: {
+    theme: {
+      type: String as PropType<"light" | "dark">,
+      default: "light",
+    },
+  },
+  setup(props) {
+    const router = useRouter();
+    const downRef = ref()
+    return () => (
+      <div
+        class={styles.theHeader}
+        style={{
+          backgroundColor: props.theme == "light" ? "#fff" : "rgba(0,0,0,.6)",
+          backdropFilter: props.theme == "light" ? "" : "blur(0.26667rem)",
+          borderBottom: props.theme == "light" ? "0.02667rem solid #f8f8f8" : "none"
+        }}
+      >
+        <a class={styles.logo} href="/">
+          <img src={props.theme == "light" ? logo : logoDark} />
+          <span class={styles.undis}>酷乐秀</span>
+        </a>
+        <Button type="primary" class={styles.btn} round size="small" onClick={() => downRef.value?.downLoadApp()}>
+          下载APP
+        </Button>
+        <Icon
+          name={props.theme == "light" ? IconSearch : IconSearchWhite}
+          color="#333"
+          size={20}
+          onClick={() => router.push("/search")}
+        />
+        <TheDown ref={downRef} />
+      </div>
+    );
+  },
+});

+ 9 - 0
src/components/TheImage/index.module.less

@@ -0,0 +1,9 @@
+.image {
+  img {
+    opacity: 0;
+    transition: all .3s;
+  }
+  img[lazy="loaded"] {
+    opacity: 1;
+  }
+}

+ 25 - 0
src/components/TheImage/index.tsx

@@ -0,0 +1,25 @@
+import { Image } from "vant";
+import { defineComponent } from "vue";
+import TheLoading from "../TheLoading";
+import styles from './index.module.less'
+
+export default defineComponent({
+  name: "TheImage",
+  props: ["src"],
+  setup(props) {
+    const imageSlots = {
+      loading: () => <TheLoading />,
+      error: () => <TheLoading />,
+    };
+    return () => (
+      <Image
+        class={styles.image}
+        width="100%"
+        height="100%"
+        lazy-load
+        src={props.src}
+        v-slots={imageSlots}
+      />
+    );
+  },
+});

+ 4 - 0
src/components/TheLoading/index.module.less

@@ -0,0 +1,4 @@
+.theLoading{
+    width: 40px;
+    height: 40px;
+}

+ 15 - 0
src/components/TheLoading/index.tsx

@@ -0,0 +1,15 @@
+import { defineComponent } from "vue";
+import { Vue3Lottie } from "vue3-lottie";
+import styles from "./index.module.less";
+import AstronautJSON from "@/assets/animate/bigLoad.json";
+
+export default defineComponent({
+  name: "TheLoading",
+  setup() {
+    return () => (
+      <div class={styles.theLoading}>
+        <Vue3Lottie animationData={AstronautJSON} speed={3} />
+      </div>
+    );
+  },
+});

+ 57 - 0
src/components/TheMusicGrid/index.module.less

@@ -0,0 +1,57 @@
+.theMusicGrid {
+  :global {
+    .van-grid {
+      margin: 0 -4px;
+    }
+    .van-grid-item {
+      width: calc(100% / 3);
+    }
+    .van-grid-item__content {
+      display: block;
+      padding: 0 4px;
+      background-color: transparent;
+    }
+  }
+  .item {
+    margin-bottom: 15px;
+    .title {
+      font-size: 14px;
+      color: #333;
+      line-height: 20px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      margin-bottom: 2px;
+    }
+    .des {
+      font-size: 12px;
+      color: #999;
+      line-height: 16px;
+    }
+  }
+  .imgWrap {
+    position: relative;
+    // height: 110px;
+    height: calc((100vw - 48px) / 3);
+    border-radius: 6px;
+    overflow: hidden;
+    margin-bottom: 6px;
+    .model {
+      position: absolute;
+      left: 4px;
+      bottom: 4px;
+      background: rgba(67, 67, 67, 0.3);
+      backdrop-filter: blur(8px);
+      display: flex;
+      align-items: center;
+      padding: 4px 6px;
+      border-radius: 20px;
+      font-size: 12px;
+      color:#fff;
+      transform: scale(.9);
+    }
+    .num{
+        margin-left: 3px;
+    }
+  }
+}

+ 44 - 0
src/components/TheMusicGrid/index.tsx

@@ -0,0 +1,44 @@
+import { Grid, GridItem, Icon, Image, Loading } from "vant";
+import { defineComponent, PropType } from "vue";
+import styles from "./index.module.less";
+import IconXin from "@/assets/icon-xin.png";
+import TheLoading from "../TheLoading";
+import TheImage from "../TheImage";
+
+export default defineComponent({
+  name: "TheMusicGrid",
+  props: {
+    list: {
+      type: Array as any,
+      default: () => [],
+    },
+  },
+  emits: ['goto'],
+  setup(props, {emit}) {
+    const imageSlots = {
+      loading: () => <TheLoading />,
+      error: () => <TheLoading />,
+    };
+    return () => (
+      <div class={styles.theMusicGrid}>
+        <Grid border={false} columnNum={3}>
+          {props.list.map((n: any) => (
+            <GridItem>
+              <div class={styles.item} onClick={() => emit('goto', n)}>
+                <div class={styles.imgWrap}>
+                  <TheImage src={n.albumCoverUrl} />
+                  <div class={styles.model}>
+                    <Icon name={IconXin} />
+                    <span class={styles.num}>{n.albumFavoriteCount}人</span>
+                  </div>
+                </div>
+                <div class={styles.title}>{n.albumName}</div>
+                <div class={styles.des}>共{n.musicSheetCount}首</div>
+              </div>
+            </GridItem>
+          ))}
+        </Grid>
+      </div>
+    );
+  },
+});

+ 9 - 0
src/components/TheSearch/index.module.less

@@ -0,0 +1,9 @@
+.theSearch{
+    :global{
+        .van-field__left-icon{
+            display: flex;
+            align-items: center;
+            margin-right: 12px;
+        }
+    }
+}

+ 45 - 0
src/components/TheSearch/index.tsx

@@ -0,0 +1,45 @@
+import { Icon, Search } from "vant";
+import { defineComponent, ref, watch } from "vue";
+import styles from "./index.module.less";
+import IconSearch from "../../assets/icon-search.png";
+
+export default defineComponent({
+  name: "TheSearch",
+  props: {
+    keyword: {
+      type: String,
+      default: "",
+    },
+  },
+  emits: ["search", 'blur', 'back'],
+  setup(props, { emit }) {
+    const keyword = ref(props.keyword);
+    watch(
+      () => props.keyword,
+      (value) => {
+        keyword.value = value;
+      }
+    );
+    const searchSlots = {
+      "left-icon": () => <Icon name={IconSearch} size={20} />,
+      action: () => (
+        <div style={{ color: "var(--van-primary-color)" }} onClick={() => emit('back')}>返回</div>
+      ),
+    };
+    return () => (
+      <div class={styles.theSearch}>
+        <Search
+          shape="round"
+          show-action
+          modelValue={keyword.value}
+          onUpdate:modelValue={(val) => (keyword.value = val)}
+          v-slots={searchSlots}
+          placeholder="搜索你想练习的曲谱和专辑"
+          onSearch={(val: string) => emit("search", val)}
+          onClear={() => emit("search", "")}
+          onBlur={() => emit('blur', keyword.value)}
+        ></Search>
+      </div>
+    );
+  },
+});

+ 65 - 0
src/components/TheSong/index.module.less

@@ -0,0 +1,65 @@
+.theSong {
+  padding: 0 10px;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0px 2px 10px 0px rgba(229, 229, 229, 0.1);
+  .item {
+    display: flex;
+    align-items: center;
+    border-bottom: 1px solid #e8e8e8;
+    padding: 16px 0;
+  }
+  .item:last-child{
+    border: none;
+  }
+  .play {
+    flex-shrink: 0;
+  }
+  .content {
+    flex: 1;
+    .top {
+      display: flex;
+      align-items: center;
+      margin-bottom: 10px;
+    }
+    .tag {
+      flex-shrink: 0;
+      padding: 2px 4px;
+      border-radius: 4px;
+    }
+    .user {
+      display: flex;
+      align-items: center;
+      .name {
+        font-size: 12px;
+        color: #999;
+        line-height: 16px;
+        margin-right: 12px;
+      }
+      .tags {
+        & > span {
+          display: inline-block;
+          background: #effbf9;
+          border-radius: 20px;
+          color: var(--van-primary-color);
+          padding: 4px 8px;
+          margin-right: 4px;
+          font-size: 12px;
+          transform: scale(.9);
+        }
+      }
+    }
+    .title {
+      max-width: 150px;
+      font-size: 16px;
+      font-weight: bold;
+      color: #1a1a1a;
+      margin: 0 6px;
+    }
+    .singer {
+      max-width: 50px;
+      font-size: 12px;
+      color: #999;
+    }
+  }
+}

+ 77 - 0
src/components/TheSong/index.tsx

@@ -0,0 +1,77 @@
+import { Icon, NoticeBar, Tag } from "vant";
+import { defineComponent, PropType } from "vue";
+import styles from "./index.module.less";
+import IconPlay from "@/assets/icon-play.png";
+import { useRouter } from "vue-router";
+export default defineComponent({
+  name: "TheSong",
+  props: {
+    list: {
+      type: Array as PropType<any[]>,
+      default: () => [],
+    },
+  },
+  setup(props) {
+    const router = useRouter();
+    const colors: any = {
+      FREE: {
+        color: "#01B84F",
+        text: "免费",
+      },
+      VIP: {
+        color: "#CD863E",
+        text: "会员",
+      },
+      CHARGE: {
+        color: "#3591CE",
+        text: "点播",
+      },
+    };
+    return () => (
+      <div class={styles.theSong}>
+        {props.list.map((n: any) => (
+          <div class={styles.item}>
+            <div class={styles.content}>
+              <div class={styles.top}>
+                <Tag
+                  style={{ color: colors[n.chargeType].color }}
+                  class={styles.tag}
+                  type="success"
+                  plain
+                >
+                  {colors[n.chargeType].text}
+                </Tag>
+                <span class={[styles.title, "van-ellipsis"]}>
+                  {n.musicSheetName}
+                </span>
+                <span class={[styles.singer, "van-ellipsis"]}>
+                  -{n.composer}
+                </span>
+              </div>
+              <div class={styles.user}>
+                {n.addName ? (
+                  <span class={styles.name}>上传者:{n.addName}</span>
+                ) : (
+                  <span class={styles.name}>作曲:{n.composer}</span>
+                )}
+                <div class={styles.tags}>
+                  {n?.subjectNames.split(",").map((name: any) => (
+                    <span>{name}</span>
+                  ))}
+                </div>
+              </div>
+            </div>
+            <div
+              style={styles.play}
+              onClick={() =>
+                router.push({ path: "/musicDetail", query: { id: n.id } })
+              }
+            >
+              <Icon name={IconPlay} size={28} />
+            </div>
+          </div>
+        ))}
+      </div>
+    );
+  },
+});

+ 29 - 0
src/components/TheTitle/index.module.less

@@ -0,0 +1,29 @@
+.theTitle{
+    display: flex;
+    align-items: center;
+    padding: 15px 0;
+    .title{
+        font-size: 18px;
+        font-weight: bold;
+        color: #1D1F26;
+    }
+    .img{
+        display: inline-block;
+        width: 21px;
+        height: 14px;
+        margin-left: 4px;
+    }
+    .more{
+        display: flex;
+        align-items: center;
+        margin-left: auto;
+        font-size: 16px;
+        color: #1D1F26;
+        font-weight: 400;
+        :global{
+            .van-icon{
+                margin-left: 6px;
+            }
+        }
+    }
+}

+ 35 - 0
src/components/TheTitle/index.tsx

@@ -0,0 +1,35 @@
+import { defineComponent } from "vue";
+import styles from "./index.module.less";
+import IconTitle from "@/assets/icon-title.png";
+import { Icon } from "vant";
+import IconArrow from '@/assets/icon-arrow.png'
+
+export default defineComponent({
+  name: "TheTitle",
+  props: {
+    title: {
+      type: String,
+    },
+    isMore: {
+      type: Boolean,
+      default: true,
+    },
+    onMore: {
+      type: Function,
+    },
+  },
+  setup(props) {
+    return () => (
+      <div class={styles.theTitle}>
+        <div class={styles.title}>{props.title}</div>
+        <img src={IconTitle} class={styles.img} />
+        {props.isMore && (
+          <div class={styles.more} onClick={props.onMore}>
+            <span>更多</span>
+            <Icon name={IconArrow} size={17} />
+          </div>
+        )}
+      </div>
+    );
+  },
+});

+ 66 - 0
src/components/TheVideoGrid/index.module.less

@@ -0,0 +1,66 @@
+.theMusicGrid {
+  :global {
+    .van-grid {
+      margin: 0 -4px;
+    }
+    .van-grid-item {
+      width: calc(100% / 2);
+    }
+    .van-grid-item__content {
+      display: block;
+      padding: 0 4px;
+      background-color: transparent;
+    }
+  }
+  .item {
+    border-radius: 6px;
+    overflow: hidden;
+    margin-bottom: 15px;
+    background-color: #fff;
+    border: 1px solid #e0e0e0;
+    .title {
+      font-size: 14px;
+      color: #333;
+      line-height: 20px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      margin-bottom: 10px;
+    }
+    .des {
+      display: flex;
+      justify-content: space-between;
+      font-size: 12px;
+      color: #999;
+      line-height: 16px;
+    }
+  }
+  .imgWrap {
+    position: relative;
+    // height: 94px;
+    height: calc((100vw - 40px) / 3.5);
+    .model {
+      position: absolute;
+      left: 0;
+      bottom: 0;
+      right: 0;
+      height: 20px;
+      background: rgba(0, 0, 0, 0.6);
+      backdrop-filter: blur(10px);
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 0 6px;
+      font-size: 12px;
+    }
+    .classNum {
+      color: #eb5e00;
+    }
+    .num {
+      color: var(--van-primary-color);
+    }
+  }
+  .itemContent {
+    padding: 8px;
+  }
+}

+ 53 - 0
src/components/TheVideoGrid/index.tsx

@@ -0,0 +1,53 @@
+import { Grid, GridItem, Icon, Image, Loading } from "vant";
+import { defineComponent, PropType } from "vue";
+import styles from "./index.module.less";
+
+export default defineComponent({
+  name: "TheVideoGrid",
+  props: {
+    list: {
+      type: Array as any,
+      default: () => [],
+    },
+  },
+  emits: ['goto'],
+  setup(props, {emit}) {
+    const imageSlots = {
+      loading: () => <Loading size={20} />,
+      error: () => <Loading size={20} />,
+    };
+    return () => (
+      <div class={styles.theMusicGrid}>
+        <Grid border={false} columnNum={2}>
+          {props.list.map((n: any) => (
+            <GridItem>
+              <div class={styles.item} onClick={() => emit('goto', n)}>
+                <div class={styles.imgWrap}>
+                  <Image
+                    width="100%"
+                    height="100%"
+                    src={n.lessonCoverUrl}
+                    v-slots={imageSlots}
+                  />
+                  <div class={styles.model}>
+                    <span class={styles.classNum}>{n.lessonCount}课时</span>
+                    <div class={styles.num}>
+                      <span class={styles.dot}></span>{n.countStudent}人在学
+                    </div>
+                  </div>
+                </div>
+                <div class={styles.itemContent}>
+                  <div class={styles.title}>{n.lessonName} {n.lessonDesc}</div>
+                  <div class={styles.des}>
+                    <span>{n.username}</span>
+                    <span>{n.lessonSubjectName}</span>
+                  </div>
+                </div>
+              </div>
+            </GridItem>
+          ))}
+        </Grid>
+      </div>
+    );
+  },
+});

+ 24 - 0
src/helpers/utils.ts

@@ -0,0 +1,24 @@
+export function chunkArr(list: [], size: number = 4) {
+  if (list.length <= 0 || size <= 0) {
+    return list;
+  }
+
+  let chunks = [];
+
+  for (let i = 0; i < list.length; i = i + size) {
+    chunks.push(list.slice(i, i + size));
+  }
+
+  return chunks;
+}
+
+export function isWeChat(): boolean {
+  //window.navigator.userAgent属性包含了浏览器类型、版本、操作系统类型、浏览器引擎类型等信息,这个属性可以用来判断浏览器类型
+  var ua = window.navigator.userAgent.toLowerCase() as any;
+  //通过正则表达式匹配ua中是否含有MicroMessenger字符串
+  if (ua.match(/MicroMessenger/i) == "micromessenger") {
+    return true;
+  } else {
+    return false;
+  }
+}

+ 2 - 3
src/main.ts

@@ -2,7 +2,6 @@ import { createApp } from 'vue'
 import './style.less'
 import App from './App'
 import router from './router'
-// import 'amfe-flexible'
+import { Lazyload } from 'vant';
 
-
-createApp(App).use(router).mount('#app')
+createApp(App).use(router).use(Lazyload).mount('#app')

+ 59 - 11
src/router/index.ts

@@ -1,14 +1,62 @@
-import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"
+import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
 
 const routes: RouteRecordRaw[] = [
-    {
-        path: '/',
-        name: 'home',
-        component: () => import('../views/home')
-    }
-]
+  {
+    path: "/",
+    name: "index",
+    component: () => import("../views/index"),
+    children: [
+      {
+        path: "",
+        name: "index",
+        component: () => import("../views/home"),
+      },
+      {
+        path: "/music",
+        name: "music",
+        component: () => import("../views/music"),
+      },
+      {
+        path: "/video",
+        name: "video",
+        component: () => import("../views/video"),
+      },
+      {
+        path: "/down",
+        name: "down",
+        component: () => import("../views/down"),
+      },
+    ],
+  },
+  {
+    path: "/search",
+    name: "search",
+    component: () => import("../views/search"),
+  },
+  {
+    path: "/videoDetail",
+    name: "videoDetail",
+    component: () => import("../views/video/detail"),
+  },
+  {
+    path: "/musicAlum",
+    name: "musicAlum",
+    component: () => import("../views/music/album"),
+  },
+  {
+    path: "/musicDetail",
+    name: "musicDetail",
+    component: () => import("../views/musicDetail"),
+  },
+];
 const router = createRouter({
-    history: createWebHashHistory(),
-    routes
-})
-export default router
+  history: createWebHashHistory(),
+  routes,
+});
+router.beforeEach((_to, _form, _next) => {
+  window.scrollTo({
+    top: 0,
+  });
+  _next();
+});
+export default router;

+ 7 - 1
src/style.less

@@ -1,11 +1,17 @@
 :root{
-  --primary-color: #fff;
+  --van-primary-color: #2DC7AA !important;
 }
 *,*::before,*::after{
   padding: 0;
   margin: 0;
+  box-sizing: border-box;
+}
+html{
+  font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Segoe UI, Arial, Roboto, "PingFang SC", "miui", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
 }
 #app{
   max-width: 750px;
   margin: 0 auto;
+  background: #F8F8F8;
+  min-height: 100vh;
 }

+ 25 - 0
src/views/components/banner/index.module.less

@@ -0,0 +1,25 @@
+.swiper {
+  min-height: 130px;
+  :global {
+    .van-swipe-item {
+      height: 130px;
+      background-color: #fff;
+      border-radius: 4px;
+    }
+    .van-swipe__indicators {
+      left: auto;
+      right: 0;
+    }
+    .van-swipe__indicator {
+      transition: all 0.3s;
+      box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.2);
+      &:not(.van-swipe__indicator--active) {
+        background: #ffffff;
+      }
+    }
+    .van-swipe__indicator--active {
+      width: 12px;
+      border-radius: 4px;
+    }
+  }
+}

+ 35 - 0
src/views/components/banner/index.tsx

@@ -0,0 +1,35 @@
+import { Image, Loading, Swipe, SwipeItem } from "vant";
+import { defineComponent, onMounted, ref } from "vue";
+import styles from "./index.module.less";
+import request from "@/helpers/request";
+import { BannerItem } from "@/views/home/type";
+import TheImage from "@/components/TheImage";
+
+export default defineComponent({
+  name: "banner",
+  setup() {
+    //轮播
+    const bannerList = ref<BannerItem[]>([]);
+    const getBannerList = async () => {
+      try {
+        const { data } = await request.get("/api-website/open/banner/list");
+        if (data && Array.isArray(data)) {
+          bannerList.value = data;
+        }
+      } catch (error) {}
+    };
+    onMounted(() => {
+      getBannerList();
+    });
+
+    return () => (
+      <Swipe class={styles.swiper} autoplay={3000}>
+        {bannerList.value.map((n: BannerItem) => (
+          <SwipeItem>
+            <TheImage src={n.coverImage} />
+          </SwipeItem>
+        ))}
+      </Swipe>
+    );
+  },
+});

+ 35 - 0
src/views/components/boutique-class/index.tsx

@@ -0,0 +1,35 @@
+import TheTitle from "@/components/TheTitle";
+import TheVideoGrid from "@/components/TheVideoGrid";
+import request from "@/helpers/request";
+import { defineComponent, onMounted, ref } from "vue";
+import { VideoItem } from "../../home/type";
+
+export default defineComponent({
+  name: "BoutiqueClass",
+  setup() {
+    //视频课
+    const videoList = ref<VideoItem[]>([]);
+    const getVideoList = async () => {
+      try {
+        const res = await request.post(
+          "/api-website/open/videoLessonGroup/page",
+          {
+            data: { albumStatus: "PASS", page: 1, rows: 4 },
+          }
+        );
+        if (res.data && Array.isArray(res.data.rows)) {
+          videoList.value = res.data.rows;
+        }
+      } catch (error) {}
+    };
+    onMounted(() => {
+      getVideoList();
+    });
+    return () => (
+      <>
+        <TheTitle title="精品视频课" />
+        <TheVideoGrid list={videoList.value} />
+      </>
+    );
+  },
+});

+ 0 - 0
src/views/components/hot-album/index.module.less


+ 54 - 0
src/views/components/hot-album/index.tsx

@@ -0,0 +1,54 @@
+import TheMusicGrid from "@/components/TheMusicGrid";
+import TheTitle from "@/components/TheTitle";
+import request from "@/helpers/request";
+import { defineComponent, onMounted, ref } from "vue";
+import { useRouter } from "vue-router";
+
+export default defineComponent({
+  name: "hotAlbum",
+  setup() {
+    const router = useRouter();
+    // 热门专辑
+    const musicAlbumList = ref<any[]>([]);
+    const getMusicAlbumList = async () => {
+      try {
+        const res = await request.post("/api-website/open/music/album/list", {
+          data: {
+            albumStatus: 1,
+            page: 1,
+            rows: 9,
+          },
+        });
+        if (res.data && Array.isArray(res.data.rows)) {
+          musicAlbumList.value = res.data.rows;
+        }
+      } catch (error) {}
+    };
+    onMounted(() => {
+      getMusicAlbumList();
+    });
+    return () => (
+      <>
+        <TheTitle
+          title="热门专辑"
+          onMore={() => {
+            router.push({
+              path: "/search",
+            });
+          }}
+        />
+        <TheMusicGrid
+          list={musicAlbumList.value}
+          onGoto={(n: any) =>
+            router.push({
+              path: "/musicAlum",
+              query: {
+                id: n.id,
+              },
+            })
+          }
+        />
+      </>
+    );
+  },
+});

+ 12 - 0
src/views/components/hot-music/index.module.less

@@ -0,0 +1,12 @@
+.hotMusic{
+    padding-bottom: 20px;
+    .title{
+        padding: 16px;
+    }
+    .swipeItem{
+        padding-left: 16px;
+    }
+    .swipeItem:last-child{
+        padding-right: 16px;
+    }
+}

+ 58 - 0
src/views/components/hot-music/index.tsx

@@ -0,0 +1,58 @@
+import TheSong from "@/components/TheSong";
+import TheTitle from "@/components/TheTitle";
+import styles from "./index.module.less";
+import request from "@/helpers/request";
+import { Swipe, SwipeItem } from "vant";
+import { defineComponent, onMounted, ref } from "vue";
+import { chunkArr } from "@/helpers/utils";
+
+export default defineComponent({
+  name: "HotMusic",
+  setup() {
+    const musicList = ref<any[]>([]);
+    
+    const getMusic = async () => {
+      try {
+        const { data } = await request.post(
+          "/api-website/open/music/sheet/list",
+          {
+            data: {
+              albumStatus: "PASS",
+              page: 1,
+              rows: 12,
+              state: 1,
+            },
+          }
+        );
+        if (data && Array.isArray(data.rows)) musicList.value = chunkArr(data.rows, 4)
+      } catch (error) {}
+    };
+    onMounted(() => {
+      getMusic();
+      getWidth();
+    });
+    const swipeWidth = ref(312);
+    const swipeShow = ref(false);
+    const getWidth = () => {
+      swipeShow.value = false;
+      const clientWidth =
+        document.body.clientWidth > 750 ? 750 : document.body.clientWidth;
+      swipeWidth.value = clientWidth - 63;
+      swipeShow.value = true;
+    };
+    return () => (
+      <div class={styles.hotMusic}>
+        <TheTitle class={styles.title} title="热门曲目" />
+        {swipeShow.value && (
+          <Swipe showIndicators={false} loop={false} width={swipeWidth.value}>
+            {musicList.value.map((n) => (
+              <SwipeItem class={styles.swipeItem}>
+                <TheSong list={n} />
+              </SwipeItem>
+            ))}
+          </Swipe>
+        )}
+      </div>
+    );
+  },
+});

+ 61 - 0
src/views/down/index.module.less

@@ -0,0 +1,61 @@
+.down {
+  min-height: 100vh;
+  background-size: 100% auto;
+  background-repeat: no-repeat;
+  background-color: #fff;
+}
+.text {
+  margin: 0 34px auto 34px;
+  padding-top: 45px;
+  .title {
+    font-size: 20px;
+    font-weight: 600;
+    color: #333;
+    line-height: 26px;
+  }
+  .item {
+    display: flex;
+    margin-top: 16px;
+  }
+  .dot {
+    display: inline-block;
+    width: 14px;
+    height: 14px;
+    background-color: var(--van-primary-color);
+    border: 4px solid #e0f7f3;
+    border-radius: 50%;
+    margin-right: 10px;
+    position: relative;
+    top: 4px;
+    flex-shrink: 0;
+  }
+  .des {
+    font-size: 14px;
+    color: #666;
+    line-height: 22px;
+  }
+}
+.logo{
+    margin-top: 200px;
+    :global{
+        .lottie-animation-container{
+            margin-left: 17px;
+        }
+    }
+}
+.app{
+    margin-top: 65px;
+    .btns{
+        display: flex;
+        justify-content: center;
+        margin: -30px 0 120px 0;
+    }
+    :global{
+        .van-button{
+            height: 37px;
+            font-size: 14px;
+            font-weight: 500;
+            margin: 0 16px;
+        }
+    }
+}

+ 46 - 0
src/views/down/index.tsx

@@ -0,0 +1,46 @@
+import { defineComponent } from "vue";
+import styles from "./index.module.less";
+import IconDonw from "@/assets/icon-donw.png";
+import IconApp from "@/assets/icon-app.png";
+import { Vue3Lottie } from "vue3-lottie";
+import "vue3-lottie/dist/style.css";
+import AstronautJSON from "@/assets/animate/kulexiuyunjiaolian.json";
+import { Button, Image } from "vant";
+import TheFooterApp from "@/components/TheFooterApp";
+
+export default defineComponent({
+  name: "down",
+  setup() {
+    return () => (
+      <div class={styles.down} style={{ backgroundImage: `url(${IconDonw})` }}>
+        <div class={styles.text}>
+          <h1 class={styles.title}>下载酷乐秀APP</h1>
+          <div class={styles.item}>
+            <span class={styles.dot}></span>
+            <span class={styles.des}>
+              酷乐秀老师端:器乐教学平台,课程信息一目了然,专业网络教室
+            </span>
+          </div>
+          <div class={styles.item}>
+            <span class={styles.dot}></span>
+            <span class={styles.des}>
+              酷乐秀学生端:您的学习好帮手,专业教师一对一,器乐练习云教练
+            </span>
+          </div>
+        </div>
+
+        <div class={styles.logo}>
+          <Vue3Lottie animationData={AstronautJSON} width={78} height={98} />
+        </div>
+        <div class={styles.app}>
+          <Image class src={IconApp} />
+          <div class={styles.btns}>
+            <Button round type="primary">下载学生端APP</Button>
+            <Button round type="primary">下载老师端APP</Button>
+          </div>
+        </div>
+        <TheFooterApp />
+      </div>
+    );
+  },
+});

+ 2 - 2
src/views/home/index.module.less

@@ -1,4 +1,4 @@
 .home{
-    color: red;
-    max-width: 750PX;
+    padding: 10px 16px;
+    
 }

+ 18 - 11
src/views/home/index.tsx

@@ -1,14 +1,21 @@
-import { Button } from "vant";
 import { defineComponent } from "vue";
-import styles from './index.module.less'
+import styles from "./index.module.less";
+import TheFooterApp from "../../components/TheFooterApp";
+
+import HotAlbum from "../components/hot-album";
+import BoutiqueClass from "../components/boutique-class";
+import Banner from "../components/banner";
 
 export default defineComponent({
-    name: 'home',
-    setup(){
-        return () => (
-            <div class={styles.home}>
-                <Button>首页</Button>
-            </div>
-        )
-    }
-})
+  name: "indexHome",
+  setup() {
+    return () => (
+      <div class={styles.home}>
+        <Banner />
+        <HotAlbum />
+        <BoutiqueClass />
+        <TheFooterApp />
+      </div>
+    );
+  },
+});

+ 30 - 0
src/views/home/type.ts

@@ -0,0 +1,30 @@
+export interface BannerItem{
+    id: number
+    coverImage: string
+    linkUrl: string
+    linkType: string
+}
+export interface MusicAlbumItem{
+    id: number
+    albumCoverUrl: string
+    albumDesc: string
+    albumFavoriteCount: number
+    albumName: string
+    albumTag: string
+    musicTagNames: string
+    subjectNames: string
+    subjectId: string | number
+    musicSheetCount: number | string
+}
+
+export interface VideoItem{
+    id: number
+    lessonName: string
+    lessonDesc: string
+    lessonCoverUrl: string
+    username: string
+    lessonSubjectName: string
+    lessonSubject: string | number
+    countStudent: number | string
+    lessonCount: number | string
+}

+ 26 - 0
src/views/index/index.module.less

@@ -0,0 +1,26 @@
+.layout {
+  .sticky {
+    position: sticky;
+    top: 0;
+    z-index: 100;
+    box-shadow: 0 4px 12px #ebedf0;
+  }
+}
+.tabs {
+  :global {
+    .van-tabs__wrap {
+      height: 40px;
+    }
+    .van-tabs__nav {
+      padding-bottom: 10px;
+    }
+    .van-tab {
+      font-size: 16px;
+      color: #666;
+      &.van-tab--active {
+        color: #333;
+        font-weight: bold;
+      }
+    }
+  }
+}

+ 42 - 0
src/views/index/index.tsx

@@ -0,0 +1,42 @@
+import { Button, Sticky, Tab, Tabs } from "vant";
+import { defineComponent, KeepAlive, ref } from "vue";
+import { RouterView, useRoute } from "vue-router";
+import TheHeader from "../../components/TheHeader";
+import styles from "./index.module.less";
+
+export default defineComponent({
+  name: "indexApp",
+  setup() {
+    const route = useRoute();
+    const active = ref(route.name as string);
+    return () => (
+      <div class={styles.layout}>
+        <div class={styles.sticky}>
+          <TheHeader />
+          <Tabs
+            class={styles.tabs}
+            active={active.value}
+            color="var(--van-primary-color)"
+            line-width="26px"
+          >
+            <Tab to="/" title="首页" name="home"></Tab>
+            <Tab to="/music" title="谱库" name="music"></Tab>
+            <Tab to="/video" title="视频" name="video"></Tab>
+            <Tab to="/down" title="下载" name="down"></Tab>
+          </Tabs>
+        </div>
+        <RouterView
+          v-slots={{
+            default: ({ Component }: any) => {
+              return (
+                <KeepAlive>
+                  <Component is={Component} />
+                </KeepAlive>
+              );
+            },
+          }}
+        ></RouterView>
+      </div>
+    );
+  },
+});

BIN=BIN
src/views/music/album/images/pan.png


BIN=BIN
src/views/music/album/images/somePan.png


+ 86 - 0
src/views/music/album/index.module.less

@@ -0,0 +1,86 @@
+.header{
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 100;
+}
+.musicContent {
+  position: relative;
+  height: 265px;
+  padding-top: 55px;
+}
+.bgImg {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+.bg {
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.6);
+  backdrop-filter: blur(10px);
+  padding: 16px;
+}
+.alumWrap {
+  display: flex;
+  align-items: center;
+  .img {
+    width: 115px;
+    height: 115px;
+    flex-shrink: 0;
+    border-radius: 6px;
+    overflow: hidden;
+    margin-right: 14px;
+  }
+  .alumTitle {
+    font-size: 18px;
+    font-weight: 500;
+    color: #fff;
+  }
+  .alumDes {
+    width: calc(100% - 129px);
+    .des {
+      color: #999;
+    }
+  }
+}
+.tags {
+  margin: 6px -2px 24px -2px;
+  .tag {
+    margin: 0 2px;
+    padding: 2px 6px;
+    color: #000;
+    background-color: rgba(113, 138, 147, 1);
+    border-radius: 20px;
+  }
+}
+.alumCollect {
+  display: flex;
+  align-items: center;
+  padding-top: 20px;
+  color: #999;
+  font-size: 14px;
+  & > img {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    margin-right: 6px;
+  }
+  .right {
+    margin-left: 26px;
+  }
+}
+.alumnContainer {
+  position: relative;
+  padding: 0 16px;
+  margin-top: -16px;
+  z-index: 10;
+  .alumnList {
+    padding: 0 12px;
+    border-radius: 6px;
+    background-color: #fff;
+  }
+}

+ 121 - 0
src/views/music/album/index.tsx

@@ -0,0 +1,121 @@
+import TheHeader from "@/components/TheHeader";
+import TheImage from "@/components/TheImage";
+import { defineComponent, onMounted, reactive, watch } from "vue";
+import styles from "./index.module.less";
+import IconPan from "./images/pan.png";
+import oStart from "@/assets/oStart.png";
+import TheTitle from "@/components/TheTitle";
+import TheSong from "@/components/TheSong";
+import request from "@/helpers/request";
+import { useRoute, useRouter } from "vue-router";
+import HotAlbum from "@/views/components/hot-album";
+import TheMusicGrid from "@/components/TheMusicGrid";
+import { Toast } from "vant";
+
+interface IState {
+  [_: string]: any;
+}
+export default defineComponent({
+  name: "musicAlumn",
+  setup() {
+    const route = useRoute();
+    const router = useRouter();
+    watch(route, () => {
+      window.scrollTo({
+        top: 0,
+      });
+      getAlumn();
+    });
+    const state = reactive<IState>({});
+    const getAlumn = async () => {
+      Toast({
+        type: "loading",
+        duration: 0,
+        message: "加载中",
+      });
+      try {
+        const { data } = await request.post(
+          "/api-website/open/music/album/detail",
+          {
+            data: {
+              id: route.query.id,
+              page: 1,
+              rows: 4,
+            },
+          }
+        );
+        Object.assign(state, data, {
+          musicTagNames: data?.musicTagNames?.split(",") || [],
+          musicSheetList: data.musicSheetList?.rows || [],
+        });
+      } catch (error) {}
+      Toast.clear();
+    };
+    onMounted(() => {
+      getAlumn();
+    });
+    return () => (
+      <>
+        <TheHeader class={styles.header} theme="dark" />
+        <div class={styles.musicContent}>
+          <img class={styles.bgImg} src={state.albumCoverUrl} />
+          <div class={styles.bg}>
+            <div class={styles.alumWrap}>
+              <div class={styles.img}>
+                <TheImage src={state.albumCoverUrl} />
+              </div>
+              <div class={styles.alumDes}>
+                <div class={[styles.alumTitle, "van-ellipsis"]}>
+                  {state.albumName}
+                </div>
+                <div class={styles.tags}>
+                  {state.musicTagNames?.map((tag: any) => (
+                    <span class={styles.tag}>{tag}</span>
+                  ))}
+                </div>
+                <div class={[styles.des, "van-multi-ellipsis--l3"]}>
+                  {state.albumDesc}
+                </div>
+              </div>
+            </div>
+            <div class={styles.alumCollect}>
+              <img src={IconPan} />
+              <span>共{state.musicSheetCount}首曲目</span>
+              <img class={styles.right} src={oStart} />
+              <span>{state.albumFavoriteCount}人收藏</span>
+            </div>
+          </div>
+        </div>
+
+        <div class={styles.alumnContainer}>
+          <div class={styles.alumnList}>
+            <TheTitle title="曲目列表" isMore={false} />
+            <TheSong list={state.musicSheetList} />
+          </div>
+
+          <TheTitle
+            title="相关专辑"
+            onMore={() => {
+              router.push({
+                path: "/search",
+              });
+            }}
+          />
+          <TheMusicGrid
+            list={state.relatedMusicAlbum}
+            onGoto={(n: any) =>
+              router.push({
+                path: "/musicAlum",
+                query: {
+                  id: n.id,
+                },
+              })
+            }
+          />
+
+          <HotAlbum />
+        </div>
+      </>
+    );
+  },
+});

+ 3 - 0
src/views/music/index.module.less

@@ -0,0 +1,3 @@
+.indexMusic{
+    padding: 16px;
+}

+ 20 - 0
src/views/music/index.tsx

@@ -0,0 +1,20 @@
+import { defineComponent } from "vue";
+import styles from "./index.module.less";
+import Banner from "../components/banner";
+import HotAlbum from "../components/hot-album";
+import HotMusic from "../components/hot-music";
+
+export default defineComponent({
+  name: "indexMusic",
+  setup() {
+    return () => (
+      <>
+        <div class={styles.indexMusic}>
+          <Banner />
+          <HotAlbum />
+        </div>
+        <HotMusic />
+      </>
+    );
+  },
+});

+ 182 - 0
src/views/musicDetail/index.module.less

@@ -0,0 +1,182 @@
+.header {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 100;
+  background-color: transparent !important;
+  backdrop-filter: none !important;
+  transition: all 0.3s;
+}
+.header.headerActive {
+  background-color: rgba(0, 0, 0, 0.6) !important;
+  backdrop-filter: blur(10px) !important;
+}
+.musicContent {
+  position: relative;
+  height: 234px;
+}
+.bgImg {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+.bg {
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.6);
+  backdrop-filter: blur(10px);
+  padding: 0 16px;
+  padding-top: 55px;
+}
+.alumWrap {
+  .alumTitle {
+    font-size: 18px;
+    font-weight: 500;
+    color: #fff;
+    padding-top: 42px;
+  }
+  .des {
+    color: #999;
+    max-width: 60%;
+  }
+}
+.musicDes {
+  padding-top: 12px;
+  display: flex;
+  justify-content: space-between;
+  .desItem {
+    padding-top: 6px;
+  }
+}
+.author {
+  display: flex;
+  align-items: center;
+  width: 146px;
+  height: 44px;
+  border-radius: 40px;
+  background-color: #fff;
+  padding: 4px;
+  .avator {
+    width: 34px;
+    height: 34px;
+    border-radius: 50%;
+    margin-right: 6px;
+  }
+  .authorName {
+    font-size: 12px;
+    color: #666;
+    flex: 1;
+    max-width: 50%;
+    line-height: 16px;
+  }
+  .by {
+    font-size: 10px;
+    color: #999;
+  }
+  :global {
+    .van-icon {
+      margin-left: auto;
+      margin-right: 10px;
+      font-weight: bold;
+      font-size: 24px;
+      color: var(--van-primary-color);
+    }
+  }
+}
+.iframe {
+  iframe {
+    border: 0;
+    width: 100%;
+    height: 350px;
+  }
+  .sheetName {
+    text-align: center;
+    font-size: 16px;
+  }
+}
+.alumnContainer {
+  position: relative;
+  padding: 0 16px;
+  padding-bottom: 100px;
+  margin-top: -16px;
+  z-index: 10;
+  .alumnList {
+    position: relative;
+    padding: 100px 12px 12px 12px;
+    border-radius: 6px;
+    background-color: #fff;
+    min-height: 100px;
+  }
+  .tag {
+    position: absolute;
+    top: 16px;
+    right: 0;
+    width: 60px;
+    height: 30px;
+    line-height: 30px;
+    background-color: #b4fbd2;
+    color: #03b851;
+    font-size: 14px;
+    border-radius: 20px 0 0 20px;
+    text-align: center;
+  }
+  :global {
+    .plyr {
+      position: initial;
+    }
+    .plyr .plyr__controls {
+      padding: 0;
+      height: 0;
+    }
+    .plyr__progress__container {
+      position: absolute;
+      top: 70px;
+      left: 12px;
+      right: 16px;
+    }
+    .plyr__control {
+      position: absolute;
+      left: 20px;
+      top: 18px;
+    }
+    .plyr__time--current {
+      position: absolute;
+      left: 64px;
+      top: 22px;
+    }
+    .plyr__time--duration {
+      display: block !important;
+      position: absolute;
+      left: 112px;
+      top: 22px;
+    }
+  }
+}
+:root {
+  --plyr-color-main: var(--van-primary-color);
+}
+.btns {
+  display: flex;
+  justify-content: center;
+  padding: 16px 0;
+  .btn {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 122px;
+    height: 36px;
+    background: #ffffff;
+    box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.06);
+    border-radius: 18px;
+    margin: 0 15px;
+    font-size: 14px;
+    & > img {
+      width: 16px;
+      height: 16px;
+      margin-right: 10px;
+    }
+  }
+}

+ 193 - 0
src/views/musicDetail/index.tsx

@@ -0,0 +1,193 @@
+import TheHeader from "@/components/TheHeader";
+import { defineComponent, onMounted, reactive, ref, watch } from "vue";
+import styles from "./index.module.less";
+import TheTitle from "@/components/TheTitle";
+import TheSong from "@/components/TheSong";
+import request from "@/helpers/request";
+import { useRoute, useRouter } from "vue-router";
+import { Icon, Toast } from "vant";
+import { useEventListener } from "@vant/use";
+import icon_teacher from "@/assets/icon_teacher.png";
+import Plyr from "plyr";
+import "plyr/dist/plyr.css";
+import TheFooterApp from "@/components/TheFooterApp";
+import oStart from "@/assets/oStart.png";
+import printIcon from "@/assets/printIcon.png";
+import Iconmusicdetail from "@/assets/icon-music-detail.png";
+import TheDown from "@/components/TheDown";
+
+interface IState {
+  [_: string]: any;
+}
+export default defineComponent({
+  name: "musicAlumn",
+  setup() {
+    const route = useRoute();
+    const router = useRouter();
+    watch(route, () => {
+      window.scrollTo({
+        top: 0,
+      });
+      getAlumn();
+    });
+    const state = reactive<IState>({});
+    const mp3 = reactive({
+      iframe: "",
+      audioFileUrl: "",
+    });
+    const getAlumn = async () => {
+      const id = route.query.id;
+      if (!id) return;
+      Toast({
+        type: "loading",
+        duration: 0,
+        message: "加载中",
+      });
+      try {
+        const { data } = await request.get(
+          `api-website/open/music/sheet/detail/${id}`
+        );
+        Object.assign(state, data);
+        if (Array.isArray(data.background) && data.background.length) {
+          const item = data.background[0];
+          mp3.audioFileUrl = item.audioFileUrl;
+          console.log(route)
+          if (location.host.includes('dev.colexiu') || location.host.includes('192.168')) {
+              mp3.iframe =  `https://dev.colexiu.com/accompany/colxiu-website.html?id=${id}&part-index=${item.id}`;
+          } else {
+              mp3.iframe =  `https://online.colexiu.com/accompany/colxiu-website.html?id=${id}&part-index=${item.id}`;
+          }
+        }
+      } catch (error) {}
+      Toast.clear();
+    };
+    onMounted(() => {
+      getAlumn();
+      initAudio();
+    });
+
+    // 滚动监听
+    const isTop = ref(true);
+    const scorllChange = () => {
+      const y = document.body.scrollTop || document.documentElement.scrollTop;
+      isTop.value = y > 55 ? false : true;
+    };
+    useEventListener("scroll", scorllChange);
+
+    const initAudio = () => {
+      const a = new Plyr("#musicAudio", {
+        controls: [
+          "play-large",
+          "play",
+          "progress",
+          "current-time",
+          "duration",
+        ],
+      });
+    };
+
+    const colors: any = {
+      FREE: {
+        color: "#01B84F",
+        text: "免费",
+      },
+      VIP: {
+        color: "#CD863E",
+        text: "会员",
+      },
+      CHARGE: {
+        color: "#3591CE",
+        text: "点播",
+      },
+    };
+
+    const downRef = ref();
+    return () => (
+      <>
+        <TheHeader
+          class={[styles.header, isTop.value ? null : styles.headerActive]}
+          theme="dark"
+        />
+        <div class={styles.musicContent}>
+          <img class={styles.bgImg} src={state.titleImg} />
+          <div class={styles.bg}>
+            <div class={styles.alumWrap}>
+              <div class={[styles.alumTitle, "van-ellipsis"]}>
+                {state.musicSheetName}
+              </div>
+              <div class={styles.musicDes}>
+                <div class={styles.des}>
+                  <div class={[styles.desItem, "van-multi-ellipsis--l2"]}>
+                    作曲:{state.composer}
+                  </div>
+                  <div class={styles.desItem}>声部:{state.subjectNames}</div>
+                </div>
+                <div class={styles.author}>
+                  <img
+                    class={styles.avator}
+                    src={state.teacher?.userAvatar || icon_teacher}
+                  />
+                  <div class={styles.authorName}>
+                    <div class="van-ellipsis">{state.teacher?.userName}</div>
+                    <div class={styles.by}>上传者</div>
+                  </div>
+                  <Icon
+                    name="plus"
+                    onClick={() => {
+                        downRef.value?.toggle();
+                    }}
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class={styles.alumnContainer}>
+          <div class={styles.alumnList}>
+            <div
+              class={styles.tag}
+              style={{ color: colors[state.chargeType]?.color }}
+            >
+              {colors[state.chargeType]?.text}
+            </div>
+            <audio id="musicAudio" src={mp3.audioFileUrl} />
+            <div class={styles.iframe}>
+              <div class={styles.sheetName}>{state.musicSheetName}</div>
+              <iframe id="musicIframe" src={mp3.iframe}></iframe>
+            </div>
+          </div>
+
+          <div class={styles.btns}>
+            <div class={styles.btn}>
+              <img src={oStart} />
+              <div>
+                <span style={{ color: "#EB5E00" }}>{state.favoriteNum}</span> 收藏
+              </div>
+            </div>
+            <div class={styles.btn} onClick={() => downRef.value?.toggle()}>
+              <img src={printIcon} />
+              <div>
+                <span style={{ color: "#EB5E00" }}></span> 下载
+              </div>
+            </div>
+          </div>
+
+          <img style={{ width: "100%" }} src={Iconmusicdetail} />
+
+          <TheTitle
+            title="TA的曲谱"
+            onMore={() => {
+              router.push({
+                path: "/search",
+              });
+            }}
+          />
+          <TheSong list={state.teacher?.musicSheetList} />
+        </div>
+        <TheFooterApp />
+        <TheDown ref={downRef} />
+      </>
+    );
+  },
+});

+ 66 - 0
src/views/search/components/search-list/Album.tsx

@@ -0,0 +1,66 @@
+import request from "@/helpers/request";
+import { defineComponent, nextTick, reactive, ref, watch } from "vue";
+import styles from "./index.module.less";
+import { useToggle } from "@vant/use";
+import { List } from "vant";
+import TheMusicGrid from "@/components/TheMusicGrid";
+import { useRoute } from "vue-router";
+
+export default defineComponent({
+  name: "searchAlbum",
+  setup() {
+    const route = useRoute();
+    watch(route, () => {
+      params.page = 1;
+      params.idAndName = route.query.search || "";
+      finish.value = false;
+      finishText.value = "暂无数据";
+      musicAlbumList.value = [];
+    });
+    const [loading, toggle] = useToggle(false);
+    const finish = ref(false);
+    const finishText = ref("暂无数据");
+    const musicAlbumList = ref<any[]>([]);
+    const params = reactive({
+      albumStatus: 1,
+      page: 1,
+      rows: 33,
+      idAndName: route.query.search || "",
+    });
+    const getMusicAlbumList = async () => {
+      if (loading.value) return;
+      toggle(true);
+      try {
+        const res = await request.post("/api-website/open/music/album/list", {
+          data: params,
+        });
+        if (res.data && Array.isArray(res.data.rows)) {
+          musicAlbumList.value = [].concat(
+            musicAlbumList.value as any,
+            res.data.rows
+          );
+          params.page++;
+          if (!res.data.rows.length) {
+            finish.value = true;
+            finishText.value = res.data.pageNo == 1 ? "暂无数据" : "没有更多了";
+          }
+        }
+      } catch (error) {}
+      nextTick(() => {
+        toggle(false);
+      });
+    };
+    return () => (
+      <div class={styles.searchAlbum}>
+        <List
+          loading={loading.value}
+          finished={finish.value}
+          finishedText={finishText.value}
+          onLoad={() => getMusicAlbumList()}
+        >
+          <TheMusicGrid list={musicAlbumList.value} />
+        </List>
+      </div>
+    );
+  },
+});

+ 68 - 0
src/views/search/components/search-list/Music.tsx

@@ -0,0 +1,68 @@
+import request from "@/helpers/request";
+import { List } from "vant";
+import { defineComponent, nextTick, reactive, ref, watch } from "vue";
+import styles from "./index.module.less";
+import { useToggle } from "@vant/use";
+import TheSong from "@/components/TheSong";
+import { useRoute } from "vue-router";
+
+export default defineComponent({
+  name: "searchMusic",
+  setup() {
+    const route = useRoute();
+    watch(route, () => {
+      params.page = 1;
+      params.idAndName = route.query.search || "";
+      finish.value = false;
+      finishText.value = "暂无数据";
+      musicList.value = [];
+      getMusic()
+    });
+    const [loading, toggle] = useToggle(false);
+    const finish = ref(false);
+    const finishText = ref("暂无数据");
+    const params = reactive({
+      albumStatus: "PASS",
+      page: 1,
+      rows: 20,
+      state: 1,
+      idAndName: route.query.search || "",
+    });
+    const musicList = ref<any[]>([]);
+    const getMusic = async () => {
+      if (loading.value) return;
+      toggle(true);
+      try {
+        const { data } = await request.post(
+          "/api-website/open/music/sheet/list",
+          {
+            data: params,
+          }
+        );
+        if (data && Array.isArray(data.rows)) {
+          musicList.value = [].concat(musicList.value as any, data.rows);
+          params.page++;
+          if (!data.rows.length) {
+            finish.value = true;
+            finishText.value = data.pageNo == 1 ? "暂无数据" : "没有更多了";
+          }
+        }
+      } catch (error) {}
+      nextTick(() => {
+        toggle(false);
+      });
+    };
+    return () => (
+      <div class={styles.searchMusic}>
+        <List
+          loading={loading.value}
+          finished={finish.value}
+          finishedText={finishText.value}
+          onLoad={() => getMusic()}
+        >
+          <TheSong list={musicList.value} />
+        </List>
+      </div>
+    );
+  },
+});

+ 22 - 0
src/views/search/components/search-list/index.module.less

@@ -0,0 +1,22 @@
+.searchList {
+  :global {
+    .van-tabs__nav {
+      background: transparent;
+    }
+    .van-tabs__wrap {
+      position: sticky;
+      top: 100px;
+      z-index: 100;
+      background: #f8f8f8;
+    }
+  }
+  .container {
+    min-height: 70vh;
+  }
+}
+.searchAlbum {
+  padding: 16px;
+}
+.searchMusic {
+  padding: 16px;
+}

+ 34 - 0
src/views/search/components/search-list/index.tsx

@@ -0,0 +1,34 @@
+import { Tab, Tabs } from "vant";
+import { defineComponent, ref } from "vue";
+import styles from "./index.module.less";
+import Album from "./Album";
+import Music from "./Music";
+export default defineComponent({
+  name: "searchList",
+  setup(props, ctx) {
+    const tabActive = ref("album");
+    return () => (
+      <div class={styles.searchList}>
+        <Tabs
+          shrink
+          v-model:active={tabActive.value}
+          color="var(--van-primary-color)"
+          line-width="26px"
+          swipeable
+          animated
+        >
+          <Tab title="专辑" name="album">
+            <div class={styles.container}>
+              <Album />
+            </div>
+          </Tab>
+          <Tab title="曲目" name="music">
+            <div class={styles.container}>
+              <Music />
+            </div>
+          </Tab>
+        </Tabs>
+      </div>
+    );
+  },
+});

+ 49 - 0
src/views/search/index.module.less

@@ -0,0 +1,49 @@
+.search {
+  .fixed{
+    position: sticky;
+    top: 0;
+    z-index: 101;
+  }
+  .top {
+    height: 50px;
+    display: flex;
+    align-items: center;
+    padding: 0 16px;
+    font-size: 16px;
+    font-weight: bold;
+    color: #fff;
+    background-color: var(--van-primary-color);
+    overflow: hidden;
+    & > img {
+      width: 25px;
+      height: 25px;
+      margin-right: 12px;
+    }
+    .topBtn {
+      margin-left: auto;
+      width: 60px;
+      height: 26px;
+      color: var(--van-primary-color);
+    }
+  }
+  .tagContent {
+    padding: 16px;
+    display: flex;
+  }
+  .tagLeft {
+    font-size: 14px;
+    color: #000;
+    white-space: nowrap;
+  }
+  .tags {
+    font-size: 14px;
+    color: #666;
+    & > span {
+      line-height: 20px;
+      margin: 0 5px;
+    }
+  }
+}
+.searchContianer{
+  padding: 0 16px;
+}

+ 87 - 0
src/views/search/index.tsx

@@ -0,0 +1,87 @@
+import { Button, Icon, Search } from "vant";
+import { defineComponent, onMounted, ref, watch } from "vue";
+import styles from "./index.module.less";
+import IconLogo1 from "../../assets/logo-1.png";
+import TheSearch from "../../components/TheSearch";
+import HotAlbum from "../components/hot-album";
+import HotMusic from "../components/hot-music";
+import request from "@/helpers/request";
+import SearchList from "./components/search-list";
+import { useRoute, useRouter } from "vue-router";
+
+export default defineComponent({
+  name: "Search",
+  setup() {
+    const route = useRoute();
+    const router = useRouter();
+    const keyword = ref((route.query.search as string) || "");
+    watch(route, () => {
+      keyword.value = (route.query.search as string) || "";
+    });
+    const hotTags = ref<any[]>([]);
+    const getTags = async () => {
+      try {
+        const { data } = await request.get(
+          "/api-website/open/music/sheet/hotTag/MUSIC"
+        );
+        if (Array.isArray(data)) hotTags.value = data;
+      } catch (error) {}
+    };
+    onMounted(() => {
+      getTags();
+    });
+
+    const onSearch = (val: string) => {
+      if (!val) {
+        keyword.value = val || "";
+        return;
+      }
+      router.replace({
+        path: "/search",
+        query: {
+          search: val,
+        },
+      });
+    };
+
+    return () => (
+      <div class={styles.search}>
+        <div class={styles.fixed}>
+          <div class={styles.top}>
+            <img class={styles.img} src={IconLogo1} />
+            <span>打开APP看海量热门乐谱</span>
+            <Button round class={styles.topBtn}>
+              打开
+            </Button>
+          </div>
+
+          <TheSearch
+            keyword={keyword.value}
+            onSearch={(val: string) => onSearch(val)}
+            onBlur={(val: string) => onSearch(val)}
+            onBack={() => {
+              router.push('/')
+            }}
+          />
+        </div>
+
+        <div class={styles.tagContent}>
+          <div class={styles.tagLeft}>热门搜索:</div>
+          <div class={styles.tags}>
+            {hotTags.value.map((n) => (
+              <span onClick={() => onSearch(n.key)}>{n.key}</span>
+            ))}
+          </div>
+        </div>
+
+        {keyword.value && <SearchList />}
+        <div style={{ display: keyword.value ? "none" : "block" }}>
+          <div class={styles.searchContianer}>
+            <HotAlbum />
+          </div>
+          <HotMusic />
+        </div>
+      </div>
+    );
+  },
+});

+ 65 - 0
src/views/video/components/video-item/index.module.less

@@ -0,0 +1,65 @@
+.theMusicGrid {
+  :global {
+    .van-grid {
+      margin: 0 -4px;
+    }
+    .van-grid-item {
+      width: 100%;
+    }
+    .van-grid-item__content {
+      display: block;
+      padding: 0 4px;
+      background-color: transparent;
+    }
+  }
+  .item {
+    border-radius: 6px;
+    overflow: hidden;
+    margin-bottom: 15px;
+    background-color: #fff;
+    border: 1px solid #e0e0e0;
+    .title {
+      font-size: 14px;
+      color: #333;
+      line-height: 20px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      margin-bottom: 10px;
+    }
+    .des {
+      display: flex;
+      justify-content: space-between;
+      font-size: 12px;
+      color: #999;
+      line-height: 16px;
+    }
+  }
+  .imgWrap {
+    position: relative;
+    height: calc((100vw - 56px) / 1.8);
+    .model {
+      position: absolute;
+      left: 0;
+      bottom: 0;
+      right: 0;
+      height: 20px;
+      background: rgba(0, 0, 0, 0.6);
+      backdrop-filter: blur(10px);
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 0 6px;
+      font-size: 12px;
+    }
+    .classNum {
+      color: #eb5e00;
+    }
+    .num {
+      color: var(--van-primary-color);
+    }
+  }
+  .itemContent {
+    padding: 8px;
+  }
+}

+ 53 - 0
src/views/video/components/video-item/index.tsx

@@ -0,0 +1,53 @@
+import { Grid, GridItem, Icon, Image, Loading } from "vant";
+import { defineComponent, PropType } from "vue";
+import styles from "./index.module.less";
+
+export default defineComponent({
+  name: "TheVideoGrid",
+  props: {
+    list: {
+      type: Array as any,
+      default: () => [],
+    },
+  },
+  emits: ['goto'],
+  setup(props, {emit}) {
+    const imageSlots = {
+      loading: () => <Loading size={20} />,
+      error: () => <Loading size={20} />,
+    };
+    return () => (
+      <div class={styles.theMusicGrid}>
+        <Grid border={false} columnNum={1}>
+          {props.list.map((n: any) => (
+            <GridItem>
+              <div class={styles.item} onClick={() => emit('goto', n)}>
+                <div class={styles.imgWrap}>
+                  <Image
+                    width="100%"
+                    height="100%"
+                    src={n.lessonCoverUrl}
+                    v-slots={imageSlots}
+                  />
+                  <div class={styles.model}>
+                    <span class={styles.classNum}>{n.lessonCount}课时</span>
+                    <div class={styles.num}>
+                      <span class={styles.dot}></span>{n.countStudent}人在学
+                    </div>
+                  </div>
+                </div>
+                <div class={styles.itemContent}>
+                  <div class={styles.title}>{n.lessonName} {n.lessonDesc}</div>
+                  <div class={styles.des}>
+                    <span>{n.username}</span>
+                    <span>{n.lessonSubjectName}</span>
+                  </div>
+                </div>
+              </div>
+            </GridItem>
+          ))}
+        </Grid>
+      </div>
+    );
+  },
+});

+ 17 - 0
src/views/video/components/video-list/index.module.less

@@ -0,0 +1,17 @@
+.videoList{
+}
+.item {
+  position: relative;
+  width: 155px;
+  height: 155px;
+  border-radius: 10px;
+  margin-bottom: 16px;
+  overflow: hidden;
+}
+.playIcon{
+    position: absolute;
+    left: 10px;
+    bottom: 10px;
+    width: 22px;
+    height: 22px;
+}

+ 33 - 0
src/views/video/components/video-list/index.tsx

@@ -0,0 +1,33 @@
+import TheImage from "@/components/TheImage";
+import { Col, Image, Row } from "vant";
+import { defineComponent, PropType } from "vue";
+import styles from "./index.module.less";
+import IconPlay from '../../image/player.png'
+import single from '../../image/single.jpg'
+
+export default defineComponent({
+  name: "videoList",
+  props: {
+    list: {
+      type: Array as PropType<any[]>,
+      default: () => [],
+    },
+  },
+  emits:['play'],
+  setup(props, {emit}) {
+    return () => (
+      <div class={styles.videoList}>
+        <Row gutter={10}>
+          {props.list.map((n) => (
+            <Col span={12}>
+              <div class={styles.item} onClick={() => emit('play', n)}>
+                <TheImage src={single} />
+                <Image class={styles.playIcon} src={IconPlay} />
+              </div>
+            </Col>
+          ))}
+        </Row>
+      </div>
+    );
+  },
+});

+ 97 - 0
src/views/video/detail/Info.tsx

@@ -0,0 +1,97 @@
+import { Button, Col, Divider, Icon, Image, Row } from "vant";
+import { defineComponent, PropType, ref } from "vue";
+import styles from "./index.module.less";
+import bookIcon from "../image/bookIcon.png";
+import tree from "../image/tree.png";
+import TheTitle from "@/components/TheTitle";
+import VideoList from "../components/video-list";
+import VideoItem from "../components/video-item";
+import TheDown from "@/components/TheDown";
+import { useRouter } from "vue-router";
+
+export default defineComponent({
+  name: "detailInfo",
+  props: {
+    videoList: {
+      type: Array as PropType<any[]>,
+      default: () => [],
+    },
+    teacher: {
+      type: Object,
+      default: () => {},
+    },
+    lessonGroup: {
+      type: Object,
+      default: () => {},
+    },
+  },
+  setup(props) {
+    const router = useRouter()
+    const downRef = ref();
+    return () => (
+      <div class={styles.detailInfo}>
+        <h2>{props.lessonGroup.lessonName}</h2>
+        <div class={styles.tags}>
+          {props.lessonGroup?.lessonSubjectName?.split(",").map((n: string) => (
+            <span class={styles.tag}>{n}</span>
+          ))}
+        </div>
+        <Row justify="space-between">
+          <Col class={styles.col} style={{ color: "#FF6422" }}>
+            <Icon name={tree} size={16} />
+            <span>共{props.lessonGroup.lessonCount}课时</span>
+          </Col>
+          <Col class={styles.col}>
+            <Icon name={bookIcon} size={16} />
+            <span>{props.lessonGroup.countStudent} 人在学</span>
+          </Col>
+        </Row>
+        <TheTitle title="课程介绍" isMore={false} />
+        <div class={styles.des}>{props.lessonGroup.lessonDesc}</div>
+
+        <Divider />
+
+        <TheTitle title="教学老师" isMore={false} />
+        <div class={styles.teacher}>
+          <Image width={68} height={68} round src={props.teacher.heardUrl} />
+          <div class={styles.teacherContent}>
+            <div class={styles.name}>{props.teacher.username}</div>
+            <div class={styles.num}>
+              <span>粉丝数</span>
+              <span>{props.teacher.fansNum}</span>
+            </div>
+          </div>
+          <Button
+            round
+            type="primary"
+            icon="plus"
+            onClick={() => downRef.value?.toggle()}
+          >
+            关注
+          </Button>
+        </div>
+
+        <TheTitle title="老师介绍" isMore={false} />
+        <div class={styles.des}>{props.teacher.introduction}</div>
+
+        <TheTitle title="老师风采" isMore={false} />
+        <VideoList
+          list={props.teacher.styleVideo}
+          onPlay={() => downRef.value?.toggle()}
+        />
+
+        <TheTitle title="其他课程" isMore={false} />
+        <VideoItem
+          list={props.videoList}
+          onGoto={(item) =>
+            router.push({
+              path: "/videoDetail",
+              query: { id: item.teacherId, groupId: item.id },
+            })
+          }
+        />
+        <TheDown ref={downRef} />
+      </div>
+    );
+  },
+});

+ 135 - 0
src/views/video/detail/index.module.less

@@ -0,0 +1,135 @@
+.sticky {
+  position: sticky;
+  top: 0;
+  z-index: 100;
+}
+.videoDetail {
+  .pic {
+    height: calc(100vw / 1.76);
+  }
+  .content {
+    position: relative;
+    padding: 0 16px 100px 16px;
+    margin-top: -16px;
+    z-index: 10;
+    .wrap {
+      border-radius: 10px;
+      overflow: hidden;
+      background-color: #fff;
+    }
+    :global {
+      .van-tab--active {
+        background-color: #fff;
+        transition: background 0.3s;
+      }
+    }
+  }
+  .wrapItem {
+    padding: 12px;
+  }
+}
+
+.detailInfo {
+  & > h2 {
+    font-size: 18px;
+    font-weight: 500;
+    color: #333;
+    margin-bottom: 10px;
+  }
+  .tags {
+    margin: 0 -4px;
+    margin-bottom: 10px;
+  }
+  .tag {
+    display: inline-block;
+    color: var(--van-primary-color);
+    font-size: 12px;
+    padding: 2px 8px;
+    margin: 0 4px;
+    background-color: #bcfdf1;
+    border-radius: 20px;
+  }
+  .col {
+    display: flex;
+    align-items: center;
+    color: var(--van-primary-color);
+    & > span {
+      margin-left: 10px;
+    }
+  }
+  .des {
+    font-size: 12px;
+    font-weight: 400;
+    color: #656565;
+    line-height: 20px;
+    white-space: pre-wrap;
+  }
+  .teacher {
+    display: flex;
+    align-items: center;
+    .teacherContent {
+      margin: 0 18px;
+      .name {
+        font-size: 16px;
+        font-weight: bold;
+        color: #000;
+        margin-bottom: 10px;
+      }
+      .num {
+        font-size: 12px;
+        color: #999;
+      }
+      .num > span:last-child {
+        font-size: 14px;
+        color: #ff8b00;
+        font-weight: 500;
+        margin-left: 5px;
+      }
+    }
+    :global {
+      .van-button {
+        margin-left: auto;
+        height: 30px;
+        font-size: 14px;
+        padding: 3px 8px;
+        background-color: #ff6969;
+        border-color: #ff6969;
+      }
+    }
+  }
+}
+
+.classWrap {
+  padding: 12px;
+  :global{
+    .van-divider:last-child{
+        display: none;
+    }
+  }
+  .classItem {
+    display: flex;
+    align-items: center;
+  }
+  .img {
+    width: 131px;
+    height: 74px;
+    margin-right: 12px;
+    border-radius: 6px;
+    overflow: hidden;
+    flex-shrink: 0;
+  }
+  .classContent{
+    width: calc(100% - 131px - 12px);
+  }
+  .classTitle {
+    font-size: 16px;
+    color: #000;
+    font-weight: bold;
+    margin-bottom: 9px;
+  }
+  .classDes {
+    font-size: 12px;
+    color: #656565;
+    line-height: 20px;
+  }
+}

+ 136 - 0
src/views/video/detail/index.tsx

@@ -0,0 +1,136 @@
+import TheDown from "@/components/TheDown";
+import TheFooterApp from "@/components/TheFooterApp";
+import TheHeader from "@/components/TheHeader";
+import TheImage from "@/components/TheImage";
+import request from "@/helpers/request";
+import { Divider, Tab, Tabs, Toast } from "vant";
+import { defineComponent, nextTick, onMounted, reactive, ref, watch } from "vue";
+import { useRoute } from "vue-router";
+import styles from "./index.module.less";
+import Info from "./Info";
+
+export default defineComponent({
+  name: "videoDetail",
+  setup() {
+    const route = useRoute();
+    watch(route, () => {
+      history.go(0)
+    })
+    const { groupId, id: teacherId } = route.query;
+    const state = reactive({
+      detailList: [] as any,
+      lessonGroup: {} as any,
+      teacher: {} as any,
+      otherList: [] as any,
+    });
+    const getDetail = async () => {
+      try {
+        const { data } = await request.get(
+          `/api-website/open/videoLessonGroup/selectVideoLesson?groupId=${groupId}`
+        );
+        state.lessonGroup = data.lessonGroup;
+        state.detailList = data.detailList;
+      } catch (error) {}
+    };
+    const getTeacher = async () => {
+      try {
+        const { data } = await request.get(
+          `api-website/open/teacher/detail/${teacherId}`
+        );
+        state.teacher = data;
+      } catch (err) {}
+    };
+    const getOther = async () => {
+      try {
+        const { data } = await request.post(
+          "/api-website/open/videoLessonGroup/otherLesson",
+          {
+            data: {
+              videoLessonGroupId: groupId,
+            },
+          }
+        );
+        state.otherList = data;
+      } catch (error) {}
+    };
+    const init = () => {
+      Toast({
+        type: 'loading',
+        message: '加载中',
+        duration:0
+      })
+      getDetail();
+      getTeacher();
+      getOther();
+      nextTick(() => {
+        setTimeout(() => {
+          Toast.clear()
+        }, 300)
+      })
+    }
+    onMounted(() => {
+      init()
+    });
+    const donwRef = ref()
+    return () => {
+      const { lessonGroup, otherList, teacher, detailList } = state;
+      return (
+        <div class={styles.videoDetail}>
+          <div class={styles.sticky}>
+            <TheHeader theme="dark" />
+          </div>
+          <div class={styles.pic}>
+            <TheImage src={lessonGroup.lessonCoverUrl} />
+          </div>
+          <div class={styles.content}>
+            <div class={styles.wrap}>
+              <Tabs lineHeight={0} background="#F7F8F7">
+                <Tab title="信息简介">
+                  <div class={styles.wrapItem}>
+                    <Info
+                      lessonGroup={lessonGroup}
+                      videoList={otherList}
+                      teacher={teacher}
+                    />
+                  </div>
+                </Tab>
+                <Tab title="课程列表">
+                  <div class={styles.classWrap}>
+                    {detailList.map(
+                      (n: any) => (
+                        <>
+                          <div class={styles.classItem} onClick={() => donwRef.value?.toggle()}>
+                            <div class={styles.img}>
+                              <TheImage src={n.coverUrl} />
+                            </div>
+                            <div class={styles.classContent}>
+                              <div class={[styles.classTitle, "van-ellipsis"]}>
+                                {n.videoTitle}
+                              </div>
+                              <div
+                                class={[
+                                  styles.classDes,
+                                  "van-multi-ellipsis--l2",
+                                ]}
+                              >
+                                {n.videoContent}
+                              </div>
+                            </div>
+                          </div>
+                          <Divider />
+                        </>
+                      )
+                    )}
+                  </div>
+                </Tab>
+              </Tabs>
+            </div>
+          </div>
+
+          <TheFooterApp />
+          <TheDown ref={donwRef} />
+        </div>
+      );
+    };
+  },
+});

BIN=BIN
src/views/video/image/bookIcon.png


BIN=BIN
src/views/video/image/player.png


BIN=BIN
src/views/video/image/single.jpg


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio