浏览代码

添加功能:专辑详情,曲谱详情,曲谱列表,支付 基础完成功能;

lex 1 年之前
父节点
当前提交
635cde6834
共有 100 个文件被更改,包括 4268 次插入161 次删除
  1. 二进制
      src/common/images/icon_checkbox-tenant.png
  2. 161 154
      src/components/col-protocol/index.tsx
  3. 17 1
      src/components/col-search/index.module.less
  4. 15 2
      src/components/col-search/index.tsx
  5. 42 0
      src/components/col-share/index.module.less
  6. 11 1
      src/components/col-share/index.tsx
  7. 26 0
      src/components/the-qrcode/index.module.less
  8. 65 0
      src/components/the-qrcode/index.tsx
  9. 18 0
      src/components/the-sticky/index.module.less
  10. 121 0
      src/components/the-sticky/index.tsx
  11. 16 0
      src/helpers/utils.ts
  12. 67 0
      src/router/index-tenant.ts
  13. 255 0
      src/router/routes-tenant.ts
  14. 69 2
      src/state.ts
  15. 7 1
      src/styles/index.less
  16. 17 0
      src/styles/tenant.less
  17. 11 0
      src/tenant/App.vue
  18. 二进制
      src/tenant/images/bg-image.png
  19. 二进制
      src/tenant/images/icon-search.png
  20. 二进制
      src/tenant/images/icon-share.png
  21. 32 0
      src/tenant/layout/auth.module.less
  22. 115 0
      src/tenant/layout/auth.tsx
  23. 二进制
      src/tenant/layout/images/bottom_bg.png
  24. 二进制
      src/tenant/layout/images/top_bg.png
  25. 44 0
      src/tenant/layout/login.module.less
  26. 210 0
      src/tenant/layout/login.tsx
  27. 80 0
      src/tenant/main.ts
  28. 二进制
      src/tenant/member-center/images/1.png
  29. 二进制
      src/tenant/member-center/images/2.png
  30. 二进制
      src/tenant/member-center/images/3.png
  31. 二进制
      src/tenant/member-center/images/4.png
  32. 二进制
      src/tenant/member-center/images/5.png
  33. 二进制
      src/tenant/member-center/images/6.png
  34. 二进制
      src/tenant/member-center/images/7.png
  35. 二进制
      src/tenant/member-center/images/8.png
  36. 二进制
      src/tenant/member-center/images/contnt-bg.png
  37. 二进制
      src/tenant/member-center/images/discount_bg.png
  38. 二进制
      src/tenant/member-center/images/function-title.png
  39. 二进制
      src/tenant/member-center/images/icon-arrow-line.png
  40. 二进制
      src/tenant/member-center/images/icon-arrow.png
  41. 二进制
      src/tenant/member-center/images/icon-logo-default.png
  42. 二进制
      src/tenant/member-center/images/icon-logo.png
  43. 二进制
      src/tenant/member-center/images/icon-member-active.png
  44. 二进制
      src/tenant/member-center/images/icon-member-s.png
  45. 二进制
      src/tenant/member-center/images/icon-selected.png
  46. 二进制
      src/tenant/member-center/images/icon_discount.png
  47. 二进制
      src/tenant/member-center/images/icon_gift.png
  48. 二进制
      src/tenant/member-center/images/icon_video.png
  49. 二进制
      src/tenant/member-center/images/info-title.png
  50. 二进制
      src/tenant/member-center/images/member-bg.png
  51. 二进制
      src/tenant/member-center/images/member_bg.png
  52. 二进制
      src/tenant/member-center/images/member_logo.png
  53. 二进制
      src/tenant/member-center/images/price-bg.png
  54. 二进制
      src/tenant/member-center/images/record_bg.png
  55. 二进制
      src/tenant/member-center/images/tip_bg.png
  56. 二进制
      src/tenant/member-center/images/vip-bg.png
  57. 563 0
      src/tenant/member-center/index.module.less
  58. 482 0
      src/tenant/member-center/index.tsx
  59. 二进制
      src/tenant/music/album-detail/charge_bg.png
  60. 二进制
      src/tenant/music/album-detail/header-bg.png
  61. 二进制
      src/tenant/music/album-detail/iStart.png
  62. 二进制
      src/tenant/music/album-detail/icon-hart-active.png
  63. 二进制
      src/tenant/music/album-detail/icon-hart.png
  64. 二进制
      src/tenant/music/album-detail/icon-menu.png
  65. 二进制
      src/tenant/music/album-detail/icon-pan.png
  66. 二进制
      src/tenant/music/album-detail/icon-start-active.png
  67. 二进制
      src/tenant/music/album-detail/icon-start.png
  68. 二进制
      src/tenant/music/album-detail/icon_music_list.png
  69. 二进制
      src/tenant/music/album-detail/icon_share.png
  70. 431 0
      src/tenant/music/album-detail/index.module.less
  71. 511 0
      src/tenant/music/album-detail/index.tsx
  72. 二进制
      src/tenant/music/album-detail/oStart.png
  73. 二进制
      src/tenant/music/album-detail/pan.png
  74. 19 0
      src/tenant/music/album/count.svg
  75. 15 0
      src/tenant/music/album/favorite.svg
  76. 13 0
      src/tenant/music/album/favorited.svg
  77. 36 0
      src/tenant/music/album/footer.tsx
  78. 18 0
      src/tenant/music/album/icon_share.svg
  79. 18 0
      src/tenant/music/album/icon_share2.svg
  80. 124 0
      src/tenant/music/album/index.module.less
  81. 383 0
      src/tenant/music/album/index.tsx
  82. 83 0
      src/tenant/music/album/item.module.less
  83. 38 0
      src/tenant/music/album/item.tsx
  84. 0 0
      src/tenant/music/component/collection/index.module.less
  85. 13 0
      src/tenant/music/component/collection/index.tsx
  86. 二进制
      src/tenant/music/component/images/collection.png
  87. 二进制
      src/tenant/music/component/images/collection_active.png
  88. 二进制
      src/tenant/music/component/images/icon-play.png
  89. 二进制
      src/tenant/music/component/images/icon-xin.png
  90. 二进制
      src/tenant/music/component/images/icon_ai.png
  91. 二进制
      src/tenant/music/component/images/icon_album.png
  92. 二进制
      src/tenant/music/component/images/icon_album_active.png
  93. 二进制
      src/tenant/music/component/images/icon_author.png
  94. 二进制
      src/tenant/music/component/images/icon_download.png
  95. 二进制
      src/tenant/music/component/images/icon_exquisite.png
  96. 二进制
      src/tenant/music/component/images/icon_music_active.png
  97. 二进制
      src/tenant/music/component/images/icon_share.png
  98. 二进制
      src/tenant/music/component/images/icon_uploader.png
  99. 71 0
      src/tenant/music/component/music-grid/index.module.less
  100. 51 0
      src/tenant/music/component/music-grid/index.tsx

二进制
src/common/images/icon_checkbox-tenant.png


+ 161 - 154
src/components/col-protocol/index.tsx

@@ -1,154 +1,161 @@
-import { Checkbox, Icon, Popup } from 'vant'
-import { defineComponent, PropType } from 'vue'
-import styles from './index.module.less'
-import activeButtonIcon from '@common/images/icon_checkbox.png'
-import inactiveButtonIcon from '@common/images/icon_checkbox_default.png'
-import ColHeader from '../col-header'
-import { state } from '@/state'
-import request from '@/helpers/request'
-const protocolText = {
-  BUY_ORDER: '《酷乐秀平台服务协议》',
-  REGISTER: '《酷乐秀平台注册协议》'
-}
-export default defineComponent({
-  name: 'protocol',
-  props: {
-    showHeader: {
-      type: Boolean,
-      default: false
-    },
-    modelValue: {
-      type: Boolean,
-      default: false
-    },
-    prototcolType: {
-      type: String as PropType<'BUY_ORDER' | 'REGISTER'>,
-      default: 'BUY_ORDER'
-    }
-  },
-  data() {
-    return {
-      exists: true,
-      checked: this.modelValue,
-      popupStatus: false,
-      protocolHTML: '',
-      protocolPopup: null as any,
-      baseUrl:
-        state.platformType === 'STUDENT' ? '/api-student' : '/api-teacher'
-    }
-  },
-  async mounted() {
-    try {
-      const res = await request.get(
-        this.baseUrl + '/sysUserContractRecord/checkContractSign',
-        {
-          params: {
-            contractType: this.prototcolType
-          }
-        }
-      )
-      // console.log(res)
-      this.exists = res.data
-      this.checked = this.checked || this.exists
-      this.$emit('update:modelValue', this.checked || this.exists)
-    } catch {}
-    this.checked = this.modelValue
-    // this.getContractDetail()
-    window.addEventListener('hashchange', this.onHash, false)
-  },
-  unmounted() {
-    window.removeEventListener('hashchange', this.onHash, false)
-  },
-  watch: {
-    checked(val) {
-      this.$emit('update:modelValue', val)
-    }
-  },
-  methods: {
-    async getContractDetail() {
-      try {
-        console.log('getContractDetail')
-        // 判断是否有协议内容
-        if (!this.protocolHTML) {
-          const res = await request.get(
-            this.baseUrl + '/sysUserContractRecord/queryContract',
-            {
-              params: {
-                contractType: this.prototcolType
-              }
-            }
-          )
-          this.protocolHTML = res.data
-          console.log(res)
-        }
-        this.onPopupClose()
-      } catch {}
-    },
-    onHash() {
-      this.popupStatus = false
-    },
-    onPopupClose() {
-      this.popupStatus = !this.popupStatus
-
-      // 打开弹窗
-      if (this.popupStatus) {
-        const route = this.$route
-        let times = 0
-        for (let i in route.query) {
-          times += 1
-        }
-        const origin = window.location.href
-        const url = times > 0 ? '&pto=' + +new Date() : '?pto=' + +new Date()
-        history.pushState('', '', `${origin}${url}`)
-      } else {
-        window.history.go(-1)
-      }
-      if (this.protocolPopup) {
-        ;(this.protocolPopup as any).scrollTop = 0
-      }
-    }
-  },
-  render() {
-    return (
-      <div class={styles.colProtocol}>
-        {!this.exists && (
-          <Checkbox
-            v-model={this.checked}
-            v-slots={{
-              icon: (props: any) => (
-                <Icon
-                  class={styles.boxStyle}
-                  name={props.checked ? activeButtonIcon : inactiveButtonIcon}
-                  size="15"
-                />
-              )
-            }}
-          >
-            我已阅读并同意
-          </Checkbox>
-        )}
-        {this.exists && <>查看</>}
-        <span onClick={this.getContractDetail} class={styles.protocolText}>
-          {protocolText[this.prototcolType]}
-        </span>
-
-        <Popup
-          ref={this.protocolPopup}
-          show={this.popupStatus}
-          position="bottom"
-          style={{ height: '100%' }}
-        >
-          {this.showHeader && <ColHeader title="酷乐秀平台服务协议" />}
-          {this.popupStatus && (
-            <div class={styles.protocolContent} id="mProtocol">
-              <div
-                class={styles.protocolContent}
-                v-html={this.protocolHTML}
-              ></div>
-            </div>
-          )}
-        </Popup>
-      </div>
-    )
-  }
-})
+import { Checkbox, Icon, Popup } from 'vant'
+import { defineComponent, PropType } from 'vue'
+import styles from './index.module.less'
+import activeButtonIcon from '@common/images/icon_checkbox.png'
+import activeButtonIconTenant from '@common/images/icon_checkbox-tenant.png'
+import inactiveButtonIcon from '@common/images/icon_checkbox_default.png'
+import ColHeader from '../col-header'
+import { state } from '@/state'
+import request from '@/helpers/request'
+const protocolText = {
+  BUY_ORDER: '《酷乐秀平台服务协议》',
+  REGISTER: '《酷乐秀平台注册协议》'
+}
+export default defineComponent({
+  name: 'protocol',
+  props: {
+    showHeader: {
+      type: Boolean,
+      default: false
+    },
+    modelValue: {
+      type: Boolean,
+      default: false
+    },
+    prototcolType: {
+      type: String as PropType<'BUY_ORDER' | 'REGISTER'>,
+      default: 'BUY_ORDER'
+    }
+  },
+  data() {
+    return {
+      exists: true,
+      checked: this.modelValue,
+      popupStatus: false,
+      protocolHTML: '',
+      protocolPopup: null as any,
+      baseUrl:
+        state.platformType === 'STUDENT' ? '/api-student' : '/api-teacher'
+    }
+  },
+  async mounted() {
+    try {
+      const res = await request.get(
+        this.baseUrl + '/sysUserContractRecord/checkContractSign',
+        {
+          params: {
+            contractType: this.prototcolType
+          }
+        }
+      )
+      // console.log(res)
+      this.exists = res.data
+      this.checked = this.checked || this.exists
+      this.$emit('update:modelValue', this.checked || this.exists)
+    } catch {}
+    this.checked = this.modelValue
+    // this.getContractDetail()
+    window.addEventListener('hashchange', this.onHash, false)
+  },
+  unmounted() {
+    window.removeEventListener('hashchange', this.onHash, false)
+  },
+  watch: {
+    checked(val) {
+      this.$emit('update:modelValue', val)
+    }
+  },
+  methods: {
+    async getContractDetail() {
+      try {
+        console.log('getContractDetail')
+        // 判断是否有协议内容
+        if (!this.protocolHTML) {
+          const res = await request.get(
+            this.baseUrl + '/sysUserContractRecord/queryContract',
+            {
+              params: {
+                contractType: this.prototcolType
+              }
+            }
+          )
+          this.protocolHTML = res.data
+          console.log(res)
+        }
+        this.onPopupClose()
+      } catch {}
+    },
+    onHash() {
+      this.popupStatus = false
+    },
+    onPopupClose() {
+      this.popupStatus = !this.popupStatus
+
+      // 打开弹窗
+      if (this.popupStatus) {
+        const route = this.$route
+        let times = 0
+        for (const i in route.query) {
+          times += 1
+        }
+        const origin = window.location.href
+        const url = times > 0 ? '&pto=' + +new Date() : '?pto=' + +new Date()
+        history.pushState('', '', `${origin}${url}`)
+      } else {
+        window.history.go(-1)
+      }
+      if (this.protocolPopup) {
+        ;(this.protocolPopup as any).scrollTop = 0
+      }
+    }
+  },
+  render() {
+    return (
+      <div class={styles.colProtocol}>
+        {!this.exists && (
+          <Checkbox
+            v-model={this.checked}
+            v-slots={{
+              icon: (props: any) => (
+                <Icon
+                  class={styles.boxStyle}
+                  name={
+                    props.checked
+                      ? state.projectType === 'tenant'
+                        ? activeButtonIconTenant
+                        : activeButtonIcon
+                      : inactiveButtonIcon
+                  }
+                  size="15"
+                />
+              )
+            }}
+          >
+            我已阅读并同意
+          </Checkbox>
+        )}
+        {this.exists && <>查看</>}
+        <span onClick={this.getContractDetail} class={styles.protocolText}>
+          {protocolText[this.prototcolType]}
+        </span>
+
+        <Popup
+          ref={this.protocolPopup}
+          show={this.popupStatus}
+          position="bottom"
+          style={{ height: '100%' }}
+        >
+          {this.showHeader && <ColHeader title="酷乐秀平台服务协议" />}
+          {this.popupStatus && (
+            <div class={styles.protocolContent} id="mProtocol">
+              <div
+                class={styles.protocolContent}
+                v-html={this.protocolHTML}
+              ></div>
+            </div>
+          )}
+        </Popup>
+      </div>
+    )
+  }
+})

+ 17 - 1
src/components/col-search/index.module.less

@@ -2,18 +2,22 @@
   .van-search {
     padding-left: 14px;
     padding-right: 14px;
+
     input {
       -webkit-user-select: text !important;
       user-select: text !important;
     }
+
     .van-search__field {
       padding: 0.13333rem var(--van-padding-xs) 0.13333rem 0 !important;
       background: transparent !important;
     }
   }
 }
+
 .col-search {
   --van-cell-background-color: transparent;
+
   // padding-left: 14px;
   // padding-right: 14px;
   :global {
@@ -21,16 +25,20 @@
       display: flex;
       align-items: center;
     }
+
     .van-field__right-icon {
       font-size: 0;
     }
+
     .van-search__action {
       display: flex;
     }
+
     .van-field__control {
       font-size: 14px;
     }
   }
+
   &.default {
     :global {
       .van-search__content {
@@ -51,12 +59,15 @@
     :global {
       .van-search__content {
         background: rgba(255, 255, 255, 0.16);
+
         input::placeholder {
           color: #fff;
         }
+
         input {
           color: #fff;
         }
+
         .van-field__clear {
           color: #fff;
         }
@@ -71,5 +82,10 @@
     font-size: 14px;
     --van-button-mini-height: 28px;
     --van-font-size-xs: 14px;
+
+    &.searchTenantBtn {
+      background: linear-gradient(270deg, #FF3C81 0%, #FF76A6 100%);
+      border: none;
+    }
   }
-}
+}

+ 15 - 2
src/components/col-search/index.tsx

@@ -2,6 +2,7 @@ import { Button, Icon, Search } from 'vant'
 import { defineComponent, PropType } from 'vue'
 import styles from './index.module.less'
 import iconSearch from '@common/images/icon_search.png'
+import iconSearchTenant from '@/tenant/images/icon-search.png'
 import iconFilter from '@common/images/icon_filter.png'
 
 type inputBackground = 'default' | 'white' | 'transparent'
@@ -9,6 +10,10 @@ type inputBackground = 'default' | 'white' | 'transparent'
 export default defineComponent({
   name: 'ColSearch',
   props: {
+    type: {
+      type: String as PropType<'person' | 'tenant'>,
+      default: ''
+    },
     modelValue: {
       type: String,
       default: ''
@@ -87,10 +92,18 @@ export default defineComponent({
           onClick={() => this.$emit('click')}
           v-slots={{
             left: () => this.$slots.left && this.$slots.left(),
-            'left-icon': () => <Icon name={this.leftIcon} size={16} />,
+            'left-icon': () => (
+              <Icon
+                name={this.type === 'tenant' ? iconSearchTenant : this.leftIcon}
+                size={16}
+              />
+            ),
             'right-icon': () => (
               <Button
-                class={styles.searchBtn}
+                class={[
+                  styles.searchBtn,
+                  this.type === 'tenant' && styles.searchTenantBtn
+                ]}
                 round
                 type="primary"
                 size="mini"

+ 42 - 0
src/components/col-share/index.module.less

@@ -4,25 +4,30 @@
   .swipe__indicators {
     color: #fff;
   }
+
   .indicators {
     display: flex;
     justify-content: center;
     align-items: center;
     margin-top: 10px;
   }
+
   .swipe__indicator {
     width: 48px;
     height: 4px;
     background-color: #fff;
     opacity: 0.5;
     border-radius: 0;
+
     &:first-child {
       border-radius: 4px 0 0 4px;
     }
+
     &:last-child {
       border-radius: 0 4px 4px 0;
     }
   }
+
   .swipe__indicator--active {
     opacity: 1;
   }
@@ -38,9 +43,11 @@
     .van-swipe {
       transform: translateZ(0);
     }
+
     .van-swipe-item {
       // margin: 0 5px;
     }
+
     // .van-swipe__indicators {
     //   border-radius: 2px;
     //   overflow: hidden;
@@ -68,6 +75,7 @@
     color: #333333;
     line-height: 35px;
   }
+
   .titleTip {
     padding-top: 5px;
     font-size: 14px;
@@ -82,18 +90,21 @@
   background: linear-gradient(270deg, #baffe7 0%, #c0dcff 100%);
   border-radius: 9px;
   color: #333;
+
   .teacherImg {
     margin-right: 12px;
     position: relative;
     width: 40px;
     text-align: center;
   }
+
   .recommend {
     position: absolute;
     height: 14px;
     left: 0;
     bottom: 3px;
   }
+
   .img {
     width: 33px;
     height: 33px;
@@ -120,49 +131,61 @@
   margin: 0 auto;
   box-sizing: border-box;
   background: #fff;
+
   &.video {
     background: url('./images/video1.png') no-repeat top center #fff;
     background-size: cover;
+
     &.yellow {
       background: url('./images/video2.png') no-repeat top center #fff;
       background-size: cover;
     }
+
     &.pink {
       background: url('./images/video3.png') no-repeat top center #fff;
       background-size: cover;
     }
   }
+
   &.live {
     background: url('./images/live1.png') no-repeat top center #fff;
     background-size: cover;
+
     &.yellow {
       background: url('./images/live2.png') no-repeat top center #fff;
       background-size: cover;
     }
+
     &.pink {
       background: url('./images/live3.png') no-repeat top center #fff;
       background-size: cover;
     }
   }
+
   &.mall {
     background: url('./images/shop1.png') no-repeat top center #fff;
     background-size: cover;
+
     &.yellow {
       background: url('./images/shop2.png') no-repeat top center #fff;
       background-size: cover;
     }
+
     &.pink {
       background: url('./images/shop3.png') no-repeat top center #fff;
       background-size: cover;
     }
   }
+
   &.music {
     background: url('./images/music1.png') no-repeat top center #fff;
     background-size: cover;
+
     &.yellow {
       background: url('./images/music2.png') no-repeat top center #fff;
       background-size: cover;
     }
+
     &.pink {
       background: url('./images/music3.png') no-repeat top center #fff;
       background-size: cover;
@@ -172,11 +195,13 @@
   &.vip {
     background: url('./images/vip1.png') no-repeat top center #fff;
     background-size: cover;
+
     &.yellow {
       background: url('./images/vip2.png') no-repeat top center #fff;
       background-size: cover;
     }
   }
+
   &.album {
     background: url('./images/album1.png') no-repeat top center #fff;
     background-size: contain;
@@ -192,6 +217,7 @@
   display: flex;
   align-items: center;
   justify-content: space-between;
+
   .logo {
     margin-left: 12px;
     padding-left: 14px;
@@ -201,6 +227,7 @@
     flex: 1;
     border-left: 1px solid #ccc;
     font-weight: 500;
+
     img {
       height: 20px;
       vertical-align: middle;
@@ -242,7 +269,22 @@
   align-items: center;
   justify-content: space-between;
   padding-top: 12px;
+
   :global(.van-button) {
     padding: 8px 46px;
   }
+
+  &.shareGroupTenantBtn {
+    :global {
+      .van-button--primary {
+        background: linear-gradient(270deg, #FF204B 0%, #FE5B71 100%);
+        border: none;
+      }
+
+      .van-button--plain.van-button--primary {
+        color: #FF204B;
+        background: #fff !important;
+      }
+    }
+  }
 }

+ 11 - 1
src/components/col-share/index.tsx

@@ -9,6 +9,10 @@ import request from '@/helpers/request'
 export default defineComponent({
   name: 'col-share',
   props: {
+    type: {
+      type: String as PropType<'default' | 'tenant'>,
+      default: 'default'
+    },
     teacherId: {
       type: Number
     },
@@ -254,7 +258,13 @@ export default defineComponent({
               </Swipe>
             </div>
 
-            <div class={['btnGroup', styles.shareGroupBtn]}>
+            <div
+              class={[
+                'btnGroup',
+                styles.shareGroupBtn,
+                this.type === 'tenant' ? styles.shareGroupTenantBtn : ''
+              ]}
+            >
               <Button
                 type="primary"
                 plain

+ 26 - 0
src/components/the-qrcode/index.module.less

@@ -0,0 +1,26 @@
+.qrcode {
+  position: relative;
+
+  .qrcodeCanvas {
+    width: 100% !important;
+    height: 100% !important;
+  }
+
+  .qrcodeLogo {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    margin-left: -20px;
+    margin-top: -20px;
+    width: 40px !important;
+    height: 40px !important;
+    border-radius: 4px;
+
+    &.small {
+      margin-left: -10px;
+      margin-top: -10px;
+      width: 20px !important;
+      height: 20px !important;
+    }
+  }
+}

+ 65 - 0
src/components/the-qrcode/index.tsx

@@ -0,0 +1,65 @@
+import { defineComponent, nextTick, onMounted, ref, watch } from 'vue'
+import logo from '@common/images/icon_logo.png'
+//@ts-ignore
+import QRCode from 'qrcode'
+import styles from './index.module.less'
+
+export default defineComponent({
+  props: {
+    text: {
+      type: String,
+      default: ''
+    },
+    size: {
+      type: String,
+      default: '200px'
+    },
+    logoSize: {
+      type: String,
+      default: 'default'
+    }
+  },
+  setup(props) {
+    const canvas = ref()
+
+    const init = () => {
+      QRCode.toCanvas(
+        canvas.value,
+        props.text,
+        {
+          margin: 1
+        },
+        (error: any) => {
+          if (error) console.log(error)
+          console.log('success')
+        }
+      )
+    }
+    watch(
+      () => props.text,
+      () => {
+        init()
+      }
+    )
+    onMounted(() => {
+      nextTick(() => {
+        init()
+      })
+    })
+    return () => (
+      <div
+        class={styles.qrcode}
+        style={{ width: props.size, height: props.size }}
+      >
+        <canvas ref={canvas} class={styles.qrcodeCanvas}></canvas>
+        <img
+          src={logo}
+          class={[
+            styles.qrcodeLogo,
+            props.logoSize === 'small' && styles.small
+          ]}
+        />
+      </div>
+    )
+  }
+})

+ 18 - 0
src/components/the-sticky/index.module.less

@@ -0,0 +1,18 @@
+.sticky {
+  position: sticky;
+  top: 0;
+  z-index: 99;
+}
+
+.white {
+  background-color: #fff;
+
+  >div {
+    padding-top: 15px;
+    box-shadow: 0px 0px 10px 0px rgba(216, 216, 216, 0.5);
+  }
+}
+
+.animationStyle {
+  transition: all .2s;
+}

+ 121 - 0
src/components/the-sticky/index.tsx

@@ -0,0 +1,121 @@
+import {
+  PropType,
+  defineComponent,
+  nextTick,
+  onMounted,
+  reactive,
+  ref,
+  watch
+} from 'vue'
+import styles from './index.module.less'
+import { useRect } from '@vant/use'
+import { useResizeObserver } from '@vueuse/core'
+
+export default defineComponent({
+  name: 'm-sticky',
+  props: {
+    position: {
+      type: String as PropType<'top' | 'bottom'>,
+      default: 'top'
+    },
+    mode: {
+      type: String as PropType<'fixed' | 'sticky'>,
+      default: 'fixed'
+    },
+    offsetTop: {
+      type: String,
+      default: '0px'
+    },
+    offsetBottom: {
+      default: '0px'
+    },
+    // 变量名
+    varName: {
+      type: String,
+      default: '--header-height'
+    }
+  },
+  emits: ['barHeight'],
+  setup(props, { slots, emit }) {
+    const forms = reactive({
+      divStyle: {} as any,
+      heightV: 0,
+      sectionStyle: {
+        width: '100%',
+        height: 'auto',
+        left: '0'
+      }
+    })
+
+    const __initHeight = (height: any) => {
+      forms.sectionStyle.height = `${height}px`
+      forms.heightV = height
+      // 设置名称
+      document.documentElement.style.setProperty(props.varName, `${height}px`)
+      emit('barHeight', height)
+    }
+
+    const divRef = ref()
+    const div2Ref = ref()
+    onMounted(() => {
+      if (props.position === 'top') {
+        forms.divStyle.top = props.offsetTop || '0px'
+      } else {
+        forms.divStyle.bottom = props.offsetBottom || '0px'
+      }
+      // const resize = new ResizeObserver(() => {
+      //   const { height } = useRect(div2Ref.value);
+      //   __initHeight(height);
+      // });
+      // resize.observe(divRef.value);
+
+      try {
+        useResizeObserver(div2Ref.value, (entries: any) => {
+          const entry = entries[0]
+          // console.log(entry, 'entry')
+          const { height } = entry.contentRect
+          if (Math.abs(height - forms.heightV) > 1) {
+            setTimeout(() => {
+              __initHeight(height)
+            }, 10)
+          }
+        })
+      } catch {
+        //
+      }
+    })
+
+    watch(
+      () => props.offsetTop,
+      () => {
+        forms.divStyle.top = props.offsetTop
+      }
+    )
+    watch(
+      () => props.offsetBottom,
+      () => {
+        forms.divStyle.bottom = props.offsetBottom
+      }
+    )
+    return () => (
+      <div
+        style={[forms.sectionStyle]}
+        class={props.mode === 'sticky' && styles.sticky}
+      >
+        <div
+          ref={divRef}
+          class={[
+            'van-sticky',
+            props.mode === 'fixed' ? 'van-sticky--fixed' : '',
+            styles.animationStyle
+          ]}
+          style={[forms.divStyle, forms.sectionStyle]}
+        >
+          <div ref={div2Ref} style={{ position: 'relative' }}>
+            {slots.default && slots.default()}
+          </div>
+        </div>
+      </div>
+    )
+  }
+})

+ 16 - 0
src/helpers/utils.ts

@@ -2,6 +2,7 @@ import dayjs from 'dayjs'
 import numeral from 'numeral'
 import { Toast } from 'vant'
 import { state as helpState } from './helpState'
+import qs from 'query-string'
 
 export const browser = () => {
   const u = navigator.userAgent
@@ -41,6 +42,7 @@ export const browser = () => {
     iPad: u.indexOf('iPad') > -1, //是否iPad
     webApp: u.indexOf('Safari') == -1, //是否web应该程序,没有头部与底部
     weixin: u.indexOf('MicroMessenger') > -1, //是否微信 (2015-01-22新增)
+    alipay: u.indexOf('AlipayClient') > -1, //是否支付宝
     huawei: !!u.match(/huawei/i) || !!u.match(/honor/i),
     xiaomi: !!u.match(/mi\s/i) || !!u.match(/redmi/i) || !!u.match(/mix/i)
   }
@@ -152,3 +154,17 @@ export const dateFormat = (
 ) => {
   return dayjs(value).format(format)
 }
+
+// 获取授权的code码
+export const getUrlCode = (name = 'code') => {
+  let search: any = {}
+  try {
+    search = {
+      ...qs.parse(location.search),
+      ...qs.parse(location.hash.split('?')[1])
+    }
+  } catch (error) {
+    //
+  }
+  return search[name]
+}

+ 67 - 0
src/router/index-tenant.ts

@@ -0,0 +1,67 @@
+import { browser } from '@/helpers/utils'
+import { state } from '@/state'
+import { Dialog } from 'vant'
+import { createRouter, createWebHashHistory, Router } from 'vue-router'
+import { postMessage } from '@/helpers/native-message'
+import routes from './routes-tenant'
+const router: Router = createRouter({
+  history: createWebHashHistory(),
+  routes
+})
+
+router.beforeEach((to, from, next) => {
+  const title = to.meta.title
+  document.title = (title || '酷乐秀') as any
+  next()
+  // if (browser().iPhone && !state.version) {
+  // try {
+  //   postMessage(
+  //     {
+  //       api: 'getVersion'
+  //     },
+  //     (res: any) => {
+  //       state.version = res.version
+  //       console.log(res, 'version')
+  //       setTimeout(() => {
+  //         next()
+  //       }, 50)
+  //     }
+  //   )
+  // } catch {}
+  // // 为了处理上面方法的没有返回
+  // setTimeout(() => {
+  // if (!state.version) {
+  //       next()
+  //     // }
+  //   // }, 5000)
+  // } else {
+  //   console.log(222)
+  //   next()
+  // }
+})
+
+let isOpen = false
+router.onError(error => {
+  if (error instanceof Error) {
+    const isChunkLoadFailed = error.name.indexOf('chunk')
+    const targetPath = router.currentRoute.value.fullPath
+    if (isChunkLoadFailed && !isOpen) {
+      isOpen = true
+      Dialog.alert({
+        title: '更新提示',
+        message: 'APP有更新请点击确定刷新页面?',
+        confirmButtonColor: 'var(--van-primary)'
+      }).then(() => {
+        // on close
+        if (browser().isApp) {
+          postMessage({ api: 'back' })
+        } else {
+          location.hash = targetPath
+          window.location.reload()
+        }
+      })
+    }
+  }
+})
+
+export default router

+ 255 - 0
src/router/routes-tenant.ts

@@ -0,0 +1,255 @@
+import Auth from '@/student/layout/auth'
+import { router, rootRouter } from './routes-common'
+
+type metaType = {
+  isRegister: boolean
+}
+
+const noLoginRouter = [
+  {
+    path: '/share-music-sheet',
+    name: 'share-music-sheet',
+    component: () => import('@/teacher/share-page/share-music-sheet/index'),
+    meta: {
+      title: '分享乐曲'
+    }
+  },
+  {
+    path: '/leaderboard',
+    component: () => import('@/student/leaderboard/index'),
+    meta: {
+      title: '曲目挑战排行榜'
+      // isExternal: true // 是否外部浏览器可以打开
+    }
+  },
+  {
+    path: '/payCenter',
+    name: 'payCenter',
+    component: () => import('@/views/adapay/pay-center'),
+    meta: {
+      title: '支付'
+    }
+  },
+  {
+    path: '/payDefine',
+    name: 'payDefine',
+    component: () => import('@/views/adapay/pay-define'),
+    meta: {
+      title: '支付'
+    }
+  },
+  {
+    path: '/payResult',
+    name: 'payResult',
+    component: () => import('@/views/adapay/pay-result'),
+    meta: {
+      title: '支付'
+    }
+  },
+  {
+    path: '/tradeDetail',
+    name: 'tradeDetail',
+    component: () => import('@/views/trade/trade-detail'),
+    meta: {
+      title: '交易详情'
+    }
+  }
+]
+
+export default [
+  {
+    path: '/',
+    component: Auth,
+    children: [
+      // ...router,
+      {
+        path: '/login',
+        name: 'login',
+        component: () => import('@/tenant/layout/login'),
+        meta: {
+          isRegister: false
+        } as metaType
+      },
+      {
+        path: '/music-album',
+        component: () => import('@/tenant/music/album/index'),
+        meta: {
+          title: '专辑'
+        }
+      },
+      {
+        path: '/music-album-detail/:id',
+        name: 'music-album-detail',
+        component: () => import('@/tenant/music/album-detail'),
+        meta: {
+          title: '专辑详情'
+        }
+      },
+      {
+        path: '/music-list',
+        component: () => import('@/tenant/music/list'),
+        meta: {
+          title: '曲谱列表'
+        }
+      },
+      {
+        path: '/train-list',
+        component: () => import('@/tenant/music/train-list'),
+        meta: {
+          title: '声部训练'
+        }
+      },
+      {
+        path: '/music-detail',
+        component: () => import('@/tenant/music/music-detail/new-index'),
+        meta: {
+          title: '曲谱详情'
+        }
+      },
+      {
+        path: '/memberCenter',
+        name: 'memberCenter',
+        component: () => import('@/tenant/member-center/index'),
+        meta: {
+          title: '会员中心'
+        }
+      },
+      {
+        path: '/orderDetail',
+        name: 'orderDetail',
+        component: () => import('@/views/order-detail/index'),
+        meta: {
+          title: '订单详情'
+        }
+      },
+      {
+        path: '/music-personal',
+        component: () => import('@/tenant/music/personal'),
+        meta: {
+          title: '我的乐谱'
+        }
+      },
+      {
+        path: '/look-album-list',
+        component: () => import('@/tenant/music/look-album-list'),
+        meta: {
+          title: '查看专辑'
+        }
+      }
+      // {
+      //   path: '/practiceClass',
+      //   name: 'practiceClass',
+      //   component: () => import('@/student/practice-class/index'),
+      //   meta: {
+      //     title: '陪练课'
+      //   }
+      // },
+      // {
+      //   path: '/videoDetail',
+      //   name: 'videoDetail',
+      //   component: () => import('@/student/video-class/video-detail'),
+      //   meta: {
+      //     title: '视频课'
+      //   }
+      // },
+      // {
+      //   path: '/videoClassDetail',
+      //   name: 'videoClassDetail',
+      //   component: () => import('@/student/video-class/video-class-detail'),
+      //   meta: {
+      //     title: '视频课详情'
+      //   }
+      // },
+      // {
+      //   path: '/liveDetail',
+      //   name: 'liveDetail',
+      //   component: () => import('@/student/live-class/live-detail'),
+      //   meta: {
+      //     title: '直播课详情'
+      //   }
+      // },
+      // {
+      //   path: '/memberActive',
+      //   name: 'memberActive',
+      //   component: () => import('@/student/member-center/member-active'),
+      //   meta: {
+      //     title: '小酷Ai会员大放价'
+      //   }
+      // },
+      // {
+      //   path: '/memberRecord',
+      //   name: 'memberRecord',
+      //   component: () => import('@/student/member-center/member-record'),
+      //   meta: {
+      //     title: '训练统计'
+      //   }
+      // },
+      // {
+      //   path: '/tradeRecord',
+      //   name: 'tradeRecord',
+      //   component: () => import('@/student/trade/index'),
+      //   meta: {
+      //     title: '交易记录'
+      //   }
+      // },
+      // {
+      //   path: '/teacherHome',
+      //   name: 'teacherHome',
+      //   component: () => import('@/student/teacher-dependent/teacher-home'),
+      //   meta: {
+      //     title: '老师主页'
+      //   }
+      // },
+      // {
+      //   path: '/teacherElegant',
+      //   name: 'teacherElegant',
+      //   component: () => import('@/student/teacher-dependent/teacher-elegant'),
+      //   meta: {
+      //     title: '老师风采'
+      //   }
+      // },
+      // {
+      //   path: '/music-upload',
+      //   component: () => import('@/teacher/music/upload'),
+      //   meta: {
+      //     title: '上传曲谱'
+      //   }
+      // },
+      // {
+      //   path: '/teacherFollow',
+      //   component: () => import('@/student/teacher-dependent/teacher-follow'),
+      //   meta: {
+      //     title: '我的关注'
+      //   }
+      // },
+      // {
+      //   path: '/track-review-activity',
+      //   component: () =>
+      //     import('@/student/share-active/track-review-activity/index'),
+      //   meta: {
+      //     title: '曲目评测活动',
+      //     isExternal: true // 是否外部浏览器可以打开
+      //   }
+      // },
+
+      // {
+      //   path: '/track-song',
+      //   component: () =>
+      //     import('@/student/share-active/track-review-activity/track-song'),
+      //   meta: {
+      //     title: '评测曲目'
+      //   }
+      // }
+    ]
+  },
+  ...noLoginRouter,
+  // ...rootRouter,
+  {
+    path: '/:pathMatch(.*)*',
+    component: () => import('@/views/404'),
+    meta: {
+      title: '404 Not Fund',
+      platform: 'STUDENT'
+    }
+  }
+]

+ 69 - 2
src/state.ts

@@ -10,12 +10,14 @@ export const state = reactive({
     data: {} as any
   },
   orchestraInfo: {
-    token: '' as any, phone: '' as any,
+    token: '' as any,
+    phone: '' as any,
     installStatus: 0 as any,
     nickname: '',
     avatar: '',
     unionId: 0 // 是否已关联账号
   } as any, // 管乐团信息
+  projectType: 'default' as 'default' | 'tenant', // 机构端,还是默认
   platformType: '' as 'STUDENT' | 'TEACHER',
   platformApi: '/api-student' as '/api-student' | '/api-teacher',
   version: '', // 版本号 例如: 1.0.0
@@ -64,4 +66,69 @@ export const openDefaultWebView = (url?: string, callBack?: any) => {
   } else {
     callBack && callBack()
   }
-}
+}
+
+/**
+ * @description 微信授权-会根据环境去判断
+ * @param wxAppId
+ * @param urlString 回调链接【默认当前页面地址】
+ * @returns void
+ */
+export const goWechatAuth = (wxAppId: string, urlString?: string) => {
+  // 开发环境
+  if (import.meta.env.DEV) {
+    const replaceUrl =
+      `https://online.colexiu.com/getWxCode?appid=${
+        wxAppId || 'wx8654c671631cfade'
+      }&state=STATE&redirect_uri=` +
+      encodeURIComponent(urlString || window.location.href)
+    window.location.replace(replaceUrl)
+  }
+
+  // 生产环境
+  if (import.meta.env.PROD) {
+    goAuth(wxAppId, urlString)
+  }
+}
+
+const goAuth = (wxAppId: string, urlString?: string) => {
+  // 用户授权
+  const urlNow = encodeURIComponent(urlString || window.location.href)
+  // console.log(urlNow, 'urlNow');
+  const scope = 'snsapi_base' //snsapi_userinfo   //静默授权 用户无感知
+  const appid = wxAppId || 'wx8654c671631cfade'
+  const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${urlNow}&response_type=code&scope=${scope}&state=STATE&connect_redirect=1#wechat_redirect`
+  window.location.replace(url)
+}
+
+/**
+ * @description 支付宝授权-会根据环境去判断
+ * @param wxAppId
+ * @param urlString 回调链接【默认当前页面地址】
+ * @returns void
+ */
+export const goAliAuth = (alipayAppId: string, urlString?: string) => {
+  // 支付宝授权
+  const urlNow = encodeURIComponent(urlString || window.location.href)
+  const appid = alipayAppId || '2021004100630808'
+  // 开发环境
+  if (import.meta.env.DEV) {
+    const url = `https://online.colexiu.com/getAliCode?app_id=${appid}&state=STATE&redirect_uri=${urlNow}`
+    window.location.replace(url)
+  }
+
+  // 生产环境
+  if (import.meta.env.PROD) {
+    alipayAuth(alipayAppId, urlString)
+  }
+}
+
+const alipayAuth = (alipayAppId: string, urlString?: string) => {
+  // 用户授权
+  const urlNow = encodeURIComponent(urlString || window.location.href)
+  const scope = 'auth_base' //snsapi_userinfo   //静默授权 用户无感知
+  const appid = alipayAppId || '2021004100630808'
+  // 判断是否是线上
+  const url = `https://openauth.alipay.com/oauth2/publicAppAuthorize.htm?app_id=${appid}&redirect_uri=${urlNow}&response_type=auth_code&scope=${scope}&state=STATE`
+  window.location.replace(url)
+}

+ 7 - 1
src/styles/index.less

@@ -122,16 +122,20 @@ body {
   padding: 0 28px;
   padding-bottom: 15px;
 }
+
 .btnMore {
   display: flex !important;
   justify-content: center !important;
+
   // :global {
   .van-button {
     width: 48% !important;
   }
-  .van-button + .van-button {
+
+  .van-button+.van-button {
     margin-left: 6px;
   }
+
   // }
 }
 
@@ -148,12 +152,14 @@ body {
 
 .sticky {
   position: relative;
+
   .van-sticky {
     height: inherit !important;
     top: var(--van-sticky-z-index) !important;
     position: fixed;
     width: 100%;
   }
+
   :global(.van-sticky--fixed) {
     box-shadow: 10px 10px 10px var(--box-shadow-color);
   }

+ 17 - 0
src/styles/tenant.less

@@ -0,0 +1,17 @@
+:root:root {
+  --van-primary: #FE2451 !important;
+  --van-picker-confirm-action-color: #FE2451 !important;
+  --van-primary-color: var(--van-primary) !important;
+  --tag-border-color: #FE2451;
+  --tag-color: #FE2451;
+}
+
+body {
+  background-color: #fafafa;
+  user-select: none;
+  margin-top: 0 !important;
+}
+
+.btnMore {
+  justify-content: space-between !important;
+}

+ 11 - 0
src/tenant/App.vue

@@ -0,0 +1,11 @@
+<template>
+  <router-view></router-view>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+  name: 'App'
+});
+</script>

二进制
src/tenant/images/bg-image.png


二进制
src/tenant/images/icon-search.png


二进制
src/tenant/images/icon-share.png


+ 32 - 0
src/tenant/layout/auth.module.less

@@ -0,0 +1,32 @@
+.error {
+  background-color: #fff;
+  display: flex;
+  // padding-top: 20px;
+  flex-direction: column;
+  min-height: calc(100vh);
+  align-items: center;
+  justify-content: center;
+  .info {
+    display: flex;
+    align-items: center;
+    margin-bottom: 30px;
+
+    span {
+      display: inline-block;
+      margin-left: 10px;
+      color: #58727e;
+      font-size: 18px;
+    }
+  }
+
+  :global {
+    .col-result-container,
+    .van-empty {
+      padding-top: 0;
+    }
+
+    .van-button {
+      width: 50%;
+    }
+  }
+}

+ 115 - 0
src/tenant/layout/auth.tsx

@@ -0,0 +1,115 @@
+import { defineComponent } from 'vue'
+import styles from './auth.module.less'
+import { state, setLogin, setLogout, setLoginError } from '@/state'
+import { browser, setAuth } from '@/helpers/utils'
+import { postMessage } from '@/helpers/native-message'
+import { RouterView } from 'vue-router'
+import { Button, Icon } from 'vant'
+import request from '@/helpers/request'
+import ColResult from '@/components/col-result'
+
+const browserInfo = browser()
+export default defineComponent({
+  name: 'Auth',
+  data() {
+    return {
+      loading: false as boolean
+    }
+  },
+  computed: {
+    isExternal() {
+      // 该路由在外部连接打开是否需要登录
+      // 只判断是否在学生端打开
+      return (this.$route.meta.isExternal && !browserInfo.isStudent) || false
+    },
+    isNeedView() {
+      return (
+        state.user.status === 'login' ||
+        this.$route.path === '/login' ||
+        (this as any).isExternal
+      )
+    }
+  },
+  mounted() {
+    !this.isExternal && this.setAuth()
+  },
+  methods: {
+    async setAuth() {
+      const { query } = this.$route
+      const token = query.userInfo || query.Authorization
+      if (token) {
+        setAuth(token)
+      }
+      if (this.loading) {
+        return
+      }
+      if (state.user.status === 'init' || state.user.status === 'error') {
+        this.loading = true
+        try {
+          let res = await request.get('/api-student/student/queryUserInfo', {
+            initRequest: true // 初始化接口
+          })
+          setLogin(res.data)
+        } catch (e: any) {
+          const message = e.message
+          if (
+            message.indexOf('403') === -1 &&
+            message.indexOf('authentication') === -1
+          ) {
+            setLoginError()
+          } else {
+            setLogout()
+          }
+        }
+        this.loading = false
+      }
+      if (state.user.status === 'logout') {
+        if (browser().isApp) {
+          postMessage({ api: 'login' })
+        } else {
+          try {
+            const route = this.$route
+            const query = {
+              returnUrl: this.$route.path,
+              ...this.$route.query
+            } as any
+            if (route.meta.isRegister) {
+              query.isRegister = route.meta.isRegister
+            }
+            this.$router.replace({
+              path: '/login',
+              query: query
+            })
+          } catch (error) {}
+        }
+      }
+    }
+  },
+  render() {
+    return (
+      <>
+        {state.user.status === 'error' ? (
+          <div class={styles.error}>
+            {/* <div class={styles.info}>
+              <Icon name="clear" size="36" color="#ee0a24" />
+              <span>加载失败,请重新尝试</span>
+            </div>
+            <Button type="primary" round onClick={this.setAuth}>
+              重新加载
+            </Button> */}
+            <ColResult
+              type="notFond"
+              classImgSize="CERT"
+              tips="加载失败,请稍后重试"
+              buttonText="重新加载"
+              plain={true}
+              onClick={this.setAuth}
+            />
+          </div>
+        ) : this.isNeedView ? (
+          <RouterView></RouterView>
+        ) : null}
+      </>
+    )
+  }
+})

二进制
src/tenant/layout/images/bottom_bg.png


二进制
src/tenant/layout/images/top_bg.png


+ 44 - 0
src/tenant/layout/login.module.less

@@ -0,0 +1,44 @@
+.login {
+  min-height: 100vh;
+  background: url('./images/top_bg.png') no-repeat top center,
+    url('./images/bottom_bg.png') no-repeat bottom center;
+  background-color: #fff;
+  background-size: 100%;
+
+  .loginTitle {
+    padding-top: 100px;
+    font-size: 26px;
+    padding-left: 35px;
+    padding-bottom: 70px;
+    line-height: 37px;
+    font-weight: 500;
+  }
+
+  .codeText {
+    color: var(--van-primary);
+  }
+
+  .margin34 {
+    margin: 0 34px;
+  }
+
+  .formTitle {
+    font-size: 18px;
+    color: #000;
+    font-weight: 500;
+  }
+
+  :global {
+    .van-cell-group {
+      margin-bottom: 35px;
+    }
+    .van-field {
+      padding-left: 0;
+      padding-right: 0;
+    }
+    .van-button + .van-button {
+      margin-top: 20px;
+      color: #000 !important;
+    }
+  }
+}

+ 210 - 0
src/tenant/layout/login.tsx

@@ -0,0 +1,210 @@
+import { defineComponent } from 'vue'
+import { CellGroup, Field, Button, CountDown, Row, Col, Toast } from 'vant'
+import ImgCode from '@/components/col-img-code'
+import { checkPhone } from '@/helpers/validate'
+import request from '@/helpers/request'
+import { setLogin, state } from '@/state'
+import { removeAuth, setAuth } from '@/helpers/utils'
+import styles from './login.module.less'
+
+type loginType = 'PWD' | 'SMS'
+export default defineComponent({
+  name: 'login',
+  data() {
+    return {
+      loginType: 'SMS' as loginType,
+      username: '',
+      password: '',
+      smsCode: '',
+      countDownStatus: true, // 是否发送验证码
+      countDownTime: 1000 * 120, // 倒计时时间
+      countDownRef: null as any, // 倒计时实例
+      imgCodeStatus: false
+    }
+  },
+  computed: {
+    codeDisable() {
+      let status = true
+      if (this.loginType === 'PWD') {
+        this.username && this.password && (status = false)
+      } else {
+        this.username && this.smsCode && (status = false)
+      }
+      return status
+    }
+  },
+  mounted() {
+    removeAuth()
+    this.directNext()
+  },
+  methods: {
+    directNext() {
+      if (state.user.status === 'login' || state.user.status === 'error') {
+        const { returnUrl, isRegister, ...rest } = this.$route.query
+        this.$router.replace({
+          path: returnUrl as any,
+          query: {
+            ...rest
+          }
+        })
+      }
+    },
+    async onLogin() {
+      try {
+        let res: any
+        if (this.loginType === 'PWD') {
+          res = await request.post('/api-auth/usernameLogin', {
+            requestType: 'form',
+            data: {
+              username: this.username,
+              password: this.password,
+              clientId: 'student',
+              clientSecret: 'student'
+            }
+          })
+        } else {
+          res = await request.post('/api-auth/smsLogin', {
+            requestType: 'form',
+            data: {
+              clientId: 'student',
+              clientSecret: 'student',
+              phone: this.username,
+              smsCode: this.smsCode
+            }
+          })
+        }
+
+        const { authentication } = res.data
+        setAuth(authentication.token_type + ' ' + authentication.access_token)
+
+        let userCash = await request.get('/api-student/student/queryUserInfo', {
+          initRequest: true // 初始化接口
+        })
+        setLogin(userCash.data)
+
+        this.directNext()
+      } catch {}
+    },
+    async onSendCode() {
+      // 发送验证码
+      if (!checkPhone(this.username)) {
+        return Toast('请输入正确的手机号码')
+      }
+      this.imgCodeStatus = true
+    },
+    onCodeSend() {
+      this.countDownStatus = false
+      this.countDownRef.start()
+    },
+    onFinished() {
+      this.countDownStatus = true
+      this.countDownRef.reset()
+    },
+    onChange() {
+      if (this.loginType === 'PWD') {
+        this.loginType = 'SMS'
+      } else if (this.loginType === 'SMS') {
+        this.loginType = 'PWD'
+      }
+    }
+  },
+  render() {
+    return (
+      <div class={styles.login}>
+        <div class={styles.loginTitle}>
+          您好,
+          <br /> 欢迎使用酷乐秀
+        </div>
+        <CellGroup class={styles.margin34} border={false}>
+          <Row style={{ marginBottom: '16px' }}>
+            <Col span={24} class={styles.formTitle}>
+              手机号
+            </Col>
+            <Col span={24} class="van-hairline--bottom">
+              <Field
+                v-model={this.username}
+                name="手机号"
+                placeholder="请输入您的手机号"
+                type="tel"
+                maxlength={11}
+              />
+            </Col>
+          </Row>
+
+          {this.loginType === 'PWD' ? (
+            <Row>
+              <Col span={24} class={styles.formTitle}>
+                密码
+              </Col>
+              <Col span={24} class="van-hairline--bottom">
+                <Field
+                  v-model={this.password}
+                  type="password"
+                  name="密码"
+                  placeholder="请输入密码"
+                />
+              </Col>
+            </Row>
+          ) : (
+            <Row>
+              <Col span={24} class={styles.formTitle}>
+                验证码
+              </Col>
+              <Col span={24} class="van-hairline--bottom">
+                <Field
+                  v-model={this.smsCode}
+                  name="验证码"
+                  placeholder="请输入验证码"
+                  type="tel"
+                  maxlength={6}
+                  // @ts-ignore
+                  vSlots={{
+                    button: () =>
+                      this.countDownStatus ? (
+                        <span class={styles.codeText} onClick={this.onSendCode}>
+                          获取验证码
+                        </span>
+                      ) : (
+                        <CountDown
+                          ref={this.countDownRef}
+                          auto-start={false}
+                          time={this.countDownTime}
+                          onFinish={this.onFinished}
+                          format="ss秒"
+                        />
+                      )
+                  }}
+                />
+              </Col>
+            </Row>
+          )}
+        </CellGroup>
+        <div class={styles.margin34}>
+          <Button
+            round
+            block
+            type="primary"
+            disabled={this.codeDisable}
+            onClick={this.onLogin}
+          >
+            提交
+          </Button>
+          <Button block round color="#F5F7FB" onClick={this.onChange}>
+            {this.loginType === 'PWD' ? '验证码登录' : '密码登录'}
+          </Button>
+        </div>
+
+        {this.imgCodeStatus ? (
+          <ImgCode
+            v-model:value={this.imgCodeStatus}
+            phone={this.username}
+            onClose={() => {
+              this.imgCodeStatus = false
+            }}
+            onSendCode={this.onCodeSend}
+          />
+        ) : null}
+      </div>
+    )
+  }
+})

+ 80 - 0
src/tenant/main.ts

@@ -0,0 +1,80 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import dayjs from 'dayjs'
+import 'dayjs/locale/zh-cn'
+import router from '../router/index-tenant'
+import vueFilter from '@/helpers/vueFilter'
+import { postMessage, promisefiyPostMessage } from '@/helpers/native-message'
+
+import 'normalize.css'
+
+import '../styles/index.less'
+import '../styles/tenant.less'
+import { state } from '@/state'
+import { browser, setAuth } from '@/helpers/utils'
+
+const app = createApp(App)
+
+// import Vconsole from 'vconsole'
+// const vconsole = new Vconsole()
+postMessage(
+  {
+    api: 'getVersion'
+  },
+  (res: any) => {
+    state.version = res.content.version
+  }
+)
+
+// 判断是否是管乐团学生端,用来获取基础数据
+if (browser().isOrchestraStudent) {
+  // await promisefiyPostMessage({
+  //   api: 'setCache',
+  //   content: {
+  //     key: 'h5-colexiu-token',
+  //     value: ''
+  //   }
+  // })
+
+  // 获取管乐团token
+  promisefiyPostMessage({ api: 'getUserAccount' }).then((res: any) => {
+    const content = res.content
+    state.orchestraInfo.token = content.token.split(' ')[1]
+    state.orchestraInfo.phone = content.phone
+    state.orchestraInfo.nickname = content.nickname
+    state.orchestraInfo.avatar = content.avatar
+    state.orchestraInfo.unionId = content.unionId || 0
+  })
+
+  // 从缓存里面获取token
+  promisefiyPostMessage({
+    api: 'getCache',
+    content: { key: 'h5-colexiu-token' }
+  }).then((res: any) => {
+    const content = res.content
+    if (content.value) {
+      setAuth(content.value)
+    }
+  })
+}
+
+if (browser().isTeacher) {
+  state.platformType = 'TEACHER'
+} else if (browser().isStudent) {
+  state.platformType = 'STUDENT'
+} else {
+  state.platformType = 'STUDENT'
+}
+if (state.platformType === 'TEACHER') {
+  state.platformApi = '/api-teacher'
+} else {
+  state.platformApi = '/api-student'
+}
+state.projectType = 'tenant'
+
+dayjs.locale('zh-ch')
+app.config.globalProperties.$dayjs = dayjs
+app.config.globalProperties.$filters = vueFilter
+app.use(router)
+
+app.mount('#app')

二进制
src/tenant/member-center/images/1.png


二进制
src/tenant/member-center/images/2.png


二进制
src/tenant/member-center/images/3.png


二进制
src/tenant/member-center/images/4.png


二进制
src/tenant/member-center/images/5.png


二进制
src/tenant/member-center/images/6.png


二进制
src/tenant/member-center/images/7.png


二进制
src/tenant/member-center/images/8.png


二进制
src/tenant/member-center/images/contnt-bg.png


二进制
src/tenant/member-center/images/discount_bg.png


二进制
src/tenant/member-center/images/function-title.png


二进制
src/tenant/member-center/images/icon-arrow-line.png


二进制
src/tenant/member-center/images/icon-arrow.png


二进制
src/tenant/member-center/images/icon-logo-default.png


二进制
src/tenant/member-center/images/icon-logo.png


二进制
src/tenant/member-center/images/icon-member-active.png


二进制
src/tenant/member-center/images/icon-member-s.png


二进制
src/tenant/member-center/images/icon-selected.png


二进制
src/tenant/member-center/images/icon_discount.png


二进制
src/tenant/member-center/images/icon_gift.png


二进制
src/tenant/member-center/images/icon_video.png


二进制
src/tenant/member-center/images/info-title.png


二进制
src/tenant/member-center/images/member-bg.png


二进制
src/tenant/member-center/images/member_bg.png


二进制
src/tenant/member-center/images/member_logo.png


二进制
src/tenant/member-center/images/price-bg.png


二进制
src/tenant/member-center/images/record_bg.png


二进制
src/tenant/member-center/images/tip_bg.png


二进制
src/tenant/member-center/images/vip-bg.png


+ 563 - 0
src/tenant/member-center/index.module.less

@@ -0,0 +1,563 @@
+.memberH {
+  :global {
+    .van-sticky {
+      background: url('./images/member-bg.png') no-repeat top center;
+      background-size: 100% 214px;
+    }
+  }
+
+}
+
+.member-center {
+  // background: url('./images/member-bg.png') no-repeat top center #FFEAE9;
+
+  background: #FFEAE9; //linear-gradient(180deg, #FFEBE9 0%, #F7C6D2 33%, #FFD0D0 100%);
+  // background-size: contain;
+  min-height: 100vh;
+  position: relative;
+
+
+
+  .bgImg {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 214px;
+    // object-fit: cover;
+    z-index: 0;
+  }
+
+  :global {
+    .van-nav-bar {
+      background-color: transparent;
+    }
+  }
+
+  .member_container {
+    position: relative;
+    // padding: 10px 14px 0;
+    background: url('./images/vip-bg.png') no-repeat center;
+    background-size: contain;
+    height: 156px;
+
+    .title {
+      display: flex;
+      align-items: center;
+      font-size: 16px;
+      line-height: 28px;
+      font-weight: 500;
+      color: #333333;
+
+      &::before {
+        content: ' ';
+        width: 4px;
+        height: 17px;
+        background: #01c1b5;
+        display: inline-block;
+        margin-right: 7px;
+        border-radius: 8px;
+      }
+    }
+
+    .iconMember {
+      position: absolute;
+      top: 16px;
+      right: 6px;
+      width: 135px;
+      height: 108px;
+    }
+  }
+
+  .level {
+    width: 44px;
+    height: 17px;
+  }
+
+  .userMember {
+    background-color: transparent;
+    width: auto;
+    padding: 0;
+    // border-radius: 10px;
+    padding: 18px 12px 30px 26px;
+
+    .userImgSection {
+      position: relative;
+      padding: 3px;
+      background: linear-gradient(270deg, rgba(255, 123, 87, 1), rgba(255, 138, 91, 1), rgba(255, 52, 96, 1));
+      margin-right: 12px;
+      border-radius: 50%;
+
+      &::before {
+        content: ' ';
+        position: absolute;
+        left: 1px;
+        top: 1px;
+        bottom: 1px;
+        right: 1px;
+        background-color: #fff;
+        border-radius: 50%;
+      }
+    }
+
+    .userImg {
+      width: 46px;
+      height: 46px;
+      border-radius: 50%;
+      vertical-align: middle;
+      overflow: hidden;
+    }
+
+    .userInfo {
+      padding-top: 4px;
+      display: flex;
+      align-items: center;
+      color: #fff;
+      padding-bottom: 5px;
+
+      .name {
+        padding-right: 5px;
+        max-width: 100px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        font-size: 18px;
+        font-weight: 600;
+        color: #742626;
+      }
+
+
+    }
+
+    .phone {
+      font-size: 13px;
+      font-weight: 500;
+      color: #91555E;
+    }
+
+    .timeRemaining {
+      margin-top: 0;
+      font-size: 14px;
+      color: #c0c0c0;
+    }
+
+  }
+
+  .member_time {
+    display: flex;
+    align-items: center;
+    padding-left: 26px;
+    font-size: 13px;
+    color: #742626;
+
+    &::after {
+      content: ' ';
+      width: 5px;
+      height: 7px;
+      background: url('./images/icon-arrow.png') no-repeat center;
+      background-size: contain;
+      margin-left: 6px;
+      margin-top: -2px;
+    }
+
+    .remaining {
+      color: #FE2451;
+      padding: 0 5px;
+      font-weight: bold;
+    }
+  }
+
+  .intro {
+    background: #FFFFFF;
+    border-radius: 16px;
+    font-size: 14px;
+    color: #bb6e3a;
+    overflow: hidden;
+
+    &::before {
+      content: ' ';
+      width: 196px;
+      height: 40px;
+      background: url('./images/info-title.png') no-repeat;
+      background-size: contain;
+      display: inline-block;
+      margin-left: 50%;
+      transform: translateX(-98px);
+    }
+
+
+    p {
+      background: linear-gradient(44deg, #FCE3E1 0%, #FEF3EC 29%, #FDDDD5 59%, #FEDBE3 100%);
+      border-radius: 12px;
+      // opacity: 0.27;
+      margin: 13px 12px 20px;
+      padding: 15px;
+      text-align: justify;
+      line-height: 22px;
+      font-size: 14px;
+      color: #2D1A18;
+
+      span {
+        color: #EF2F56;
+      }
+    }
+  }
+
+
+  .memberContainer {
+    background: rgba(255, 255, 255, 0.5);
+    border-radius: 20px;
+    border: 2px solid #FFFFFF;
+    position: relative;
+    // margin-top: -15px;
+    padding: 12px 12px 12px;
+    margin-bottom: 12px;
+    z-index: 99;
+  }
+
+
+  .memberItem {
+    padding-top: 20px;
+
+
+
+    .title {
+      font-size: 16px;
+      color: #333333;
+      font-weight: 500;
+
+      span {
+        color: #f7b500;
+      }
+    }
+  }
+
+  .vipFunction {
+    margin-top: 12px;
+    border-radius: 16px;
+    background-color: #fff;
+    padding: 0 12px 20px;
+    overflow: hidden;
+
+    &::before {
+      content: ' ';
+      width: 196px;
+      height: 40px;
+      background: url('./images/function-title.png') no-repeat;
+      background-size: contain;
+      display: inline-block;
+      margin-left: 50%;
+      transform: translateX(-98px);
+    }
+  }
+
+  .member_function {
+    display: flex;
+    justify-content: space-between;
+    flex-wrap: wrap;
+
+    .function_item__content {
+      height: 100%;
+    }
+
+    .function_item {
+      width: 76px;
+      padding: 12px 0;
+      margin-top: 8px;
+      border-radius: 8px;
+      overflow: hidden;
+      background-color: #faefe3;
+      text-align: center;
+    }
+
+    .function_text {
+      font-size: 12px;
+      color: #814014;
+      line-height: 16px;
+    }
+  }
+
+  .system-list::-webkit-scrollbar {
+    display: none;
+    /* Chrome Safari */
+  }
+
+  .system-list {
+    width: 100%;
+    overflow-x: auto;
+    overflow-y: hidden;
+    display: flex;
+    position: relative;
+    user-select: none;
+    box-sizing: content-box;
+    margin-bottom: 20px;
+    height: auto;
+    transition: all .2s;
+
+    &.systemHide {
+      height: 0;
+      transition: all .2s;
+      margin-bottom: 0px;
+    }
+  }
+
+  .system-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    flex: 1 0 auto;
+    width: 96px;
+    min-height: 120px;
+    box-sizing: border-box;
+    background: #ffffff;
+    border-radius: 12px;
+    border: 1px solid #e5e5e5;
+    margin-left: 10px;
+
+    &:last-child {
+      margin-right: 10px;
+    }
+
+    .title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333333;
+      line-height: 20px;
+    }
+
+    .price {
+      color: #EF2F56;
+      font-size: 25px;
+      line-height: 1.5;
+      font-family: DINAlternate-Bold, DINAlternate;
+      font-weight: bold;
+
+      span {
+        font-size: 16px;
+      }
+    }
+
+    .originalPrice {
+      color: #999999;
+      font-size: 13px;
+    }
+
+    &.active {
+      background: linear-gradient(223deg, #FEECE3 0%, #FEE4E3 52%, #FFDCE6 100%);
+      border: 1px solid #FF4264;
+      position: relative;
+
+      .title {
+        color: #EF2F56;
+      }
+
+
+      .originalPrice {
+        color: #EF2F56;
+      }
+
+      &::before {
+        content: ' ';
+        font: 14px/1 'vant-icon';
+        width: 27px;
+        height: 18px;
+
+        position: absolute;
+        top: -1px;
+        right: -1px;
+        background: url('./images/icon-selected.png') no-repeat center;
+        background-size: contain;
+      }
+    }
+  }
+
+  .bottom_function {
+    background: url('./images/price-bg.png') no-repeat top center #fff;
+    background-size: contain;
+    box-shadow: inset 0px 1px 0px 0px #FFFFFF;
+    border-radius: 16px 16px 0px 0px;
+    border: 2px solid #FFFFFF;
+  }
+
+  .memberMeal {
+    .titleMeal {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      font-size: 16px;
+      font-weight: 600;
+      color: #131415;
+      line-height: 22px;
+      padding: 16px 15px;
+
+      .iconArrowLine {
+        display: inline-block;
+        width: 14px;
+        height: 14px;
+        background: url('./images/icon-arrow-line.png') no-repeat center;
+        background-size: contain;
+      }
+    }
+  }
+
+  .btnGroup {
+    // position: fixed;
+    // bottom: 0;
+    // left: 0;
+    // right: 0;
+    // z-index: 100;
+    background-color: #fff;
+    display: flex;
+    align-items: center;
+    padding: 0 16px 12px;
+    justify-content: space-between;
+
+    .btn {
+      padding: 0 22px;
+      height: 44px;
+      color: #fff !important;
+      background: linear-gradient(270deg, #FF204B 0%, #FE5B71 100%);
+      border-radius: 39px;
+      font-size: 18px;
+      font-weight: bold;
+      border: none;
+
+      .unit {
+        font-size: 14px;
+      }
+    }
+
+    .priceSection {
+      display: flex;
+      align-items: center;
+      font-size: 16px;
+      color: #1a1a1a;
+
+      .price {
+        font-size: 18px;
+        font-weight: bold;
+        color: #ff3535;
+
+        .priceUnit {
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+
+.memberDiscount {
+  margin-top: 16px;
+  position: relative;
+  background: url('./images/discount_bg.png') no-repeat center;
+  background-size: contain;
+  display: flex;
+  align-items: center;
+  height: 44px;
+  font-size: 16px;
+  color: #ff7100;
+  line-height: 18px;
+
+  .discountAvatar {
+    margin-left: 15px;
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    overflow: hidden;
+    border: 1px solid #ffaf59;
+  }
+
+  .discountName {
+    padding-left: 30px;
+    max-width: 200px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .discountGift {
+    position: absolute;
+    right: 26px;
+    top: 7px;
+    width: 29px;
+    height: 29px;
+  }
+}
+
+.discountItem {
+  height: 14px;
+  padding-bottom: 2px;
+
+  img {
+    height: 100%;
+  }
+}
+
+.discountBuy {
+  height: 18px;
+  padding-bottom: 0;
+  margin-left: 8px;
+}
+
+.shareBtn {
+  display: flex;
+  align-items: flex-start;
+  color: #666;
+  font-size: 14px;
+  line-height: 20px !important;
+
+  :global(.van-image) {
+    width: 18px;
+    height: 18px;
+    margin-right: 6px;
+  }
+}
+
+.shareVip {
+  position: relative;
+  margin-top: 50px;
+  display: flex;
+  flex: 1;
+  align-items: center;
+  padding: 11px 6px 11px;
+  background: #ffffff;
+  border-radius: 10px;
+
+  .icon {
+    width: 36px;
+    height: 36px;
+    border-radius: 10px;
+  }
+
+  .info {
+    margin-left: 6px;
+    flex: 1;
+    word-break: break-all;
+
+    >h4 {
+      color: var(--music-list-item-title-color);
+      font-size: 14px;
+      font-weight: 600;
+    }
+
+    >p {
+      color: var(--music-list-item-mate-color);
+      line-height: 17px;
+    }
+  }
+}
+
+.tagDiscount {
+  position: absolute;
+  top: -23px;
+  left: 15px;
+  padding: 0 10px;
+  height: 23px;
+  background: linear-gradient(180deg, #ffb635 0%, #ff4e18 100%);
+  border-radius: 8px 8px 0px 0px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #ffffff;
+  line-height: 24px;
+}

+ 482 - 0
src/tenant/member-center/index.tsx

@@ -0,0 +1,482 @@
+import ColHeader from '@/components/col-header'
+import { Button, Cell, Icon, Image, Popup, Toast } from 'vant'
+import { defineComponent } from 'vue'
+import styles from './index.module.less'
+import request from '@/helpers/request'
+import { setLogin, state } from '@/state'
+import iconStudent from '@common/images/icon_student.png'
+import iconTeacher from '@common/images/icon_teacher.png'
+import iconGift from './images/icon_gift.png'
+import iconShare from '../music/album/icon_share2.svg'
+import iconDiscount from './images/icon_discount.png'
+import iconMemberLogo from './images/member_logo.png'
+import iconLogo from './images/icon-logo-default.png'
+import iconLogoActive from './images/icon-logo.png'
+import iconMember from './images/icon-member-s.png'
+import iconMmeberActive from './images/icon-member-active.png'
+import { orderStatus } from '@/views/order-detail/orderStatus'
+import dayjs from 'dayjs'
+import { memberType } from '@/constant'
+import { moneyFormat } from '@/helpers/utils'
+import ColShare from '@/components/col-share'
+import TheSticky from '@/components/the-sticky'
+import bgImg from './images/member-bg.png'
+
+export const getAssetsHomeFile = (fileName: string) => {
+  const path = `./images/${fileName}`
+  const modules = import.meta.globEager('./images/*')
+  return modules[path].default
+}
+
+export default defineComponent({
+  name: 'MemberCenter',
+  data() {
+    const query = this.$route.query
+    return {
+      activityId: query.activityId,
+      recomUserId: query.recomUserId,
+      apiSuffix:
+        state.platformType === 'STUDENT' ? '/api-student' : '/api-teacher',
+      agreeStatus: false,
+      functionList: [],
+      memberList: [],
+      selectMember: {} as any,
+      params: {
+        page: 1,
+        rows: 20
+      },
+      discountTeacher: {
+        avatar: '',
+        discount: 0,
+        username: ''
+      },
+      shareStatus: false,
+      shareUrl: '',
+      shareDiscount: 0, // 是否有优惠活动
+      hidePriceStatus: false
+    }
+  },
+  computed: {
+    userInfo() {
+      const users = state.user.data
+      return {
+        username: users?.username,
+        phone: users?.phone,
+        avatar: users?.heardUrl,
+        id: users?.userId,
+        memberRankSettingId: users?.memberRankSettingId,
+        isVip: users?.isVip,
+        membershipDays: users?.membershipDays,
+        membershipEndTime: users?.membershipEndTime
+      }
+    }
+  },
+  async mounted() {
+    try {
+      const userInfo = await request.get(
+        state.platformType === 'TEACHER'
+          ? '/api-teacher/teacher/queryUserInfo'
+          : '/api-student/student/queryUserInfo'
+      )
+      setLogin(userInfo.data)
+
+      const res = await request.post(
+        `${this.apiSuffix}/memberPriceSettings/vipPermissions`
+      )
+      const result = res.data || []
+      this.functionList = result.map((item: any) => {
+        return {
+          title: item.paramName,
+          icon: getAssetsHomeFile(`${item.paramValue}.png`)
+        }
+      })
+
+      const setting = await request.post(
+        `${this.apiSuffix}/memberPriceSettings/list`,
+        {
+          data: {
+            activityId: Number(this.activityId),
+            userId: this.recomUserId
+          }
+        }
+      )
+      const { list, ...more } = setting.data
+      this.discountTeacher = {
+        ...more
+      }
+      const settingResult = list || []
+      let settingList: any = []
+      settingResult.forEach((item: any) => {
+        const tempItem = {
+          title: '',
+          salePrice: item.salePrice,
+          originalPrice: item.originalPrice,
+          period: item.period,
+          id: item.id,
+          discount: item.discount,
+          discountPrice: item.discountPrice,
+          status: false
+        }
+
+        tempItem.title = memberType[item.period]
+
+        item.period !== 'DAY' && settingList.push(tempItem)
+      })
+
+      settingList = settingList ? settingList.reverse() : []
+      if (settingList.length > 0) {
+        settingList[0].status = true
+        this.selectMember = settingList[0]
+      }
+      this.memberList = settingList
+    } catch {}
+  },
+  methods: {
+    // async onShare() {
+    //   try {
+    //     const res = await request.post('/api-teacher/open/vipProfit', {
+    //       data: {
+    //         userId: this.userInfo.id
+    //       }
+    //     })
+
+    //     this.shareUrl = `${location.origin}/teacher#/shareVip?recomUserId=${this.userInfo.id}&userType=${state.platformType}`
+    //     // 判断是否有我分享的编号
+    //     if (res.data && res.data.activityId) {
+    //       this.shareUrl = this.shareUrl + '&activityId=' + res.data.activityId
+    //     }
+    //     this.shareStatus = true
+    //     this.shareDiscount = res.data.discount || 0
+    //     return
+    //   } catch {
+    //     //
+    //   }
+    // },
+    calcSalePrice(item: any) {
+      // discount
+      if (item.discount === 1) {
+        const tempPrice = Number(
+          (item.salePrice - item.discountPrice).toFixed(2)
+        )
+        return tempPrice >= 0 ? tempPrice : 0
+      }
+      return item.salePrice
+    },
+    onSubmit() {
+      const member: any = this.selectMember
+      // 判断是否有会员
+      const startTime = this.userInfo.isVip
+        ? dayjs(this.userInfo.membershipEndTime).toDate()
+        : new Date()
+      let endTime = new Date()
+      if (member.period === 'MONTH') {
+        endTime = dayjs(startTime).add(1, 'month').toDate()
+      } else if (member.period === 'QUARTERLY') {
+        endTime = dayjs(startTime).add(3, 'month').toDate()
+      } else if (member.period === 'YEAR_HALF') {
+        endTime = dayjs(startTime).add(6, 'month').toDate()
+      } else if (member.period === 'YEAR') {
+        endTime = dayjs(startTime).add(1, 'year').toDate()
+      }
+
+      orderStatus.orderObject.orderType = 'VIP'
+      orderStatus.orderObject.orderName = '小酷Ai' + member.title
+      orderStatus.orderObject.orderDesc = '小酷Ai' + member.title
+      orderStatus.orderObject.actualPrice = this.calcSalePrice(member)
+      orderStatus.orderObject.recomUserId = this.recomUserId
+      orderStatus.orderObject.activityId = this.activityId
+      orderStatus.orderObject.orderNo = ''
+      orderStatus.orderObject.orderList = [
+        {
+          orderType: 'VIP',
+          goodsName: '小酷Ai' + member.title,
+          id: member.id,
+          title: member.title,
+          price: this.calcSalePrice(member),
+          startTime: dayjs(startTime).format('YYYY-MM-DD'),
+          endTime: dayjs(endTime).format('YYYY-MM-DD'),
+          recomUserId: this.recomUserId
+        }
+      ]
+      this.$router.push({
+        path: '/orderDetail',
+        query: {
+          orderType: 'VIP'
+        }
+      })
+    }
+  },
+  render() {
+    return (
+      <div class={styles['member-center']}>
+        <div class={styles.memberH}>
+          <TheSticky position="top">
+            <ColHeader
+              background={'transparent'}
+              color={'#fff'}
+              border={false}
+              isFixed={false}
+              // v-slots={{
+              //   right: () => (
+              //     <div class={styles.shareBtn} onClick={this.onShare}>
+              //       <Image src={iconShare} />
+              //       分享
+              //     </div>
+              //   )
+              // }}
+            />
+          </TheSticky>
+        </div>
+        <img class={styles.bgImg} src={bgImg} />
+        <div class={styles.member_container}>
+          {this.userInfo.isVip ? (
+            <img src={iconMmeberActive} class={styles.iconMember} />
+          ) : (
+            <img src={iconMember} class={styles.iconMember} />
+          )}
+
+          <Cell
+            class={styles.userMember}
+            labelClass={styles.timeRemaining}
+            v-slots={{
+              icon: () => (
+                <div class={styles.userImgSection}>
+                  <Image
+                    class={styles.userImg}
+                    src={this.userInfo.avatar || iconStudent}
+                    fit="cover"
+                  />
+                </div>
+              ),
+              title: () => (
+                <div class={styles.userInfo}>
+                  <span class={styles.name}>{this.userInfo.username}</span>
+                  <Image
+                    class={styles.level}
+                    src={this.userInfo.isVip ? iconLogoActive : iconLogo}
+                  />
+                </div>
+              ),
+              label: () => (
+                <span
+                  class={styles.phone}
+                  v-html={`${this.userInfo.phone}`}
+                ></span>
+              )
+            }}
+          ></Cell>
+
+          <div class={styles.member_time}>
+            {this.userInfo.isVip ? (
+              <div>
+                会员权益有效期剩余
+                <span class={styles.remaining}>
+                  {this.userInfo.membershipDays}
+                </span>
+                天
+              </div>
+            ) : (
+              <div>亲,您还不是平台会员哦</div>
+            )}
+          </div>
+        </div>
+
+        <div class={styles.memberContainer}>
+          <div class={[styles.intro]}>
+            <p>
+              酷乐秀会员可使用包括平台提供的所有训练乐谱,并
+              <span>专享“小酷AI”八大核心功能</span>
+              ,让孩子<span>在家就能轻松完成乐器自主规范练习</span>,让家长
+              <span>即时掌握练习情况</span>。
+            </p>
+          </div>
+
+          {this.functionList.length > 0 && (
+            <div class={[styles.memberItem, styles.vipFunction]}>
+              <div class={styles.member_function}>
+                {this.functionList.map((item: any) => (
+                  <div class={styles.function_item}>
+                    <Icon name={item.icon} size={34} />
+                    <div class={styles.function_text} v-html={item.title}></div>
+                  </div>
+                ))}
+              </div>
+            </div>
+          )}
+
+          {/*  */}
+
+          {/* <ColProtocol
+            v-model={this.agreeStatus}
+            showHeader
+            style={{ paddingLeft: 0, paddingRight: 0, marginBottom: '64px' }}
+          /> */}
+        </div>
+        {this.memberList.length > 0 && (
+          <TheSticky position="bottom">
+            <div
+              class={styles.bottom_function}
+              style={{ visibility: 'hidden' }}
+            >
+              <div class={styles.memberMeal}>
+                <div class={styles.titleMeal}>
+                  <span>会员套餐</span>
+                  <i
+                    class={[
+                      styles.iconArrowLine,
+                      this.hidePriceStatus && styles.iconArrowLineHide
+                    ]}
+                    onClick={() =>
+                      (this.hidePriceStatus = !this.hidePriceStatus)
+                    }
+                  ></i>
+                </div>
+
+                {!this.hidePriceStatus && (
+                  <div class={styles['system-list']}>
+                    {this.memberList.map((item: any) => (
+                      <div
+                        class={[
+                          styles['system-item'],
+                          item.status && styles.active
+                        ]}
+                        onClick={() => {
+                          this.memberList.forEach((item: any) => {
+                            item.status = false
+                          })
+                          item.status = true
+                          this.selectMember = item
+                        }}
+                      >
+                        <p class={styles.title}>{item.title}</p>
+                        <p class={styles.price}>
+                          <span>¥</span>
+                          {moneyFormat(this.calcSalePrice(item), '0,0[.]00')}
+                        </p>
+                        <del class={styles.originalPrice}>
+                          ¥{moneyFormat(item.originalPrice, '0,0[.]00')}
+                        </del>
+                      </div>
+                    ))}
+                  </div>
+                )}
+              </div>
+              <div class={styles.btnGroup}>
+                <Button round block class={styles.btn} onClick={this.onSubmit}>
+                  <span class={styles.unit}>¥</span>
+                  {(this as any).$filters.moneyFormat(
+                    this.calcSalePrice(this.selectMember) || 0
+                  )}
+                  元立即开通
+                </Button>
+              </div>
+            </div>
+            <div
+              class={styles.bottom_function}
+              style={{ position: 'absolute', bottom: 0, left: 0, right: 0 }}
+            >
+              <div class={styles.memberMeal}>
+                <div class={styles.titleMeal}>
+                  <span>会员套餐</span>
+                  <i
+                    class={[
+                      styles.iconArrowLine,
+                      this.hidePriceStatus && styles.iconArrowLineHide
+                    ]}
+                    onClick={() =>
+                      (this.hidePriceStatus = !this.hidePriceStatus)
+                    }
+                  ></i>
+                </div>
+
+                <div
+                  class={[
+                    styles['system-list'],
+                    this.hidePriceStatus && styles.systemHide
+                  ]}
+                >
+                  {this.memberList.map((item: any) => (
+                    <div
+                      class={[
+                        styles['system-item'],
+                        item.status && styles.active
+                      ]}
+                      onClick={() => {
+                        this.memberList.forEach((item: any) => {
+                          item.status = false
+                        })
+                        item.status = true
+                        this.selectMember = item
+                      }}
+                    >
+                      {/* <div class={styles.discountItem}>
+                        {item.discount == 1 && <img src={iconDiscount} />}
+                      </div> */}
+                      <p class={styles.title}>{item.title}</p>
+                      <p class={styles.price}>
+                        <span>¥</span>
+                        {moneyFormat(this.calcSalePrice(item), '0,0[.]00')}
+                      </p>
+                      <del class={styles.originalPrice}>
+                        ¥{moneyFormat(item.originalPrice, '0,0[.]00')}
+                      </del>
+                    </div>
+                  ))}
+                </div>
+              </div>
+              <div class={styles.btnGroup}>
+                {/* <div class={styles.priceSection}>
+                  支付金额:
+                  <div class={styles.price}>
+                    <span class={styles.priceUnit}>¥</span>
+                    <span class={styles.priceNum}>
+                      {(this as any).$filters.moneyFormat(
+                        this.calcSalePrice(this.selectMember) || 0
+                      )}
+                    </span>
+                  </div>
+                  {this.selectMember?.discount == 1 && (
+                    <div class={[styles.discountItem, styles.discountBuy]}>
+                      <img src={iconDiscount} />
+                    </div>
+                  )}
+                </div> */}
+                <Button round block class={styles.btn} onClick={this.onSubmit}>
+                  <span class={styles.unit}>¥</span>
+                  {(this as any).$filters.moneyFormat(
+                    this.calcSalePrice(this.selectMember) || 0
+                  )}
+                  元立即开通
+                </Button>
+              </div>
+            </div>
+          </TheSticky>
+        )}
+
+        {/* <Popup
+          v-model:show={this.shareStatus}
+          style={{ background: 'transparent' }}
+        >
+          <ColShare
+            teacherId={this.userInfo.id}
+            shareUrl={this.shareUrl}
+            shareType="vip"
+            shareLength={2}
+          >
+            <div class={styles.shareVip}>
+              {this.shareDiscount === 1 && (
+                <div class={styles.tagDiscount}>专属优惠</div>
+              )}
+
+              <img class={styles.icon} src={iconMemberLogo} />
+              <div class={styles.info}>
+                <h4 class="van-multi-ellipsis--l2">小酷Ai会员</h4>
+                <p>海量曲谱、智能评测,专为器乐学习者量身打造</p>
+              </div>
+            </div>
+          </ColShare>
+        </Popup> */}
+      </div>
+    )
+  }
+})

二进制
src/tenant/music/album-detail/charge_bg.png


二进制
src/tenant/music/album-detail/header-bg.png


二进制
src/tenant/music/album-detail/iStart.png


二进制
src/tenant/music/album-detail/icon-hart-active.png


二进制
src/tenant/music/album-detail/icon-hart.png


二进制
src/tenant/music/album-detail/icon-menu.png


二进制
src/tenant/music/album-detail/icon-pan.png


二进制
src/tenant/music/album-detail/icon-start-active.png


二进制
src/tenant/music/album-detail/icon-start.png


二进制
src/tenant/music/album-detail/icon_music_list.png


二进制
src/tenant/music/album-detail/icon_share.png


+ 431 - 0
src/tenant/music/album-detail/index.module.less

@@ -0,0 +1,431 @@
+.base>div {
+  background: url(./header-bg.png) no-repeat top center;
+  // background-attachment: fixed;
+}
+
+.detail {
+  overflow: hidden;
+
+  --van-nav-bar-background-color: transparent;
+  --van-nav-bar-icon-color: #fff;
+  --van-nav-bar-text-color: #fff;
+  --van-nav-bar-title-text-color: #fff;
+}
+
+.base {
+  :global(.van-sticky--fixed) {
+    box-shadow: 10px 10px 10px var(--box-shadow-color);
+  }
+}
+
+.img {
+  width: 94px;
+  height: 94px;
+  margin-right: 18px;
+  position: relative;
+
+  >img,
+  >div {
+    position: absolute;
+    border-radius: 10px;
+    overflow: hidden;
+  }
+
+  &::before {
+    content: '';
+    width: 94px;
+    height: 94px;
+    border-radius: 50%;
+    // background-color: var(--music-list-item-background-color);
+    // box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.08);
+    background: linear-gradient(180deg, #434343 0%, #666666 50%, #434343 100%);
+    position: absolute;
+    right: -9px;
+    top: 0px;
+  }
+
+  .albumType {
+    position: absolute;
+    left: 0;
+    top: 0;
+    background: linear-gradient(180deg, #ff8900 0%, #ff5100 100%);
+    box-shadow: 0px 1px 2px 0px rgba(150, 13, 0, 0.11);
+    border-radius: 10px 0px 10px 0px;
+    font-size: 12px;
+    padding: 0 6px;
+    line-height: 20px;
+    color: #ffffff;
+    z-index: 9;
+  }
+}
+
+.shareBtn {
+  display: flex;
+  align-items: flex-start;
+  color: #fff;
+  font-size: 14px;
+  line-height: 20px !important;
+
+  :global(.van-image) {
+    width: 18px;
+    height: 18px;
+    margin-right: 6px;
+  }
+}
+
+.detailContent {
+  background-color: white;
+  padding: 0 14px;
+  border-radius: 17px 17px 0px 0px;
+
+  .main {
+    padding-top: 24px;
+    padding-bottom: 20px;
+    display: flex;
+  }
+
+  .favoriteContaineer {
+    border: none;
+    color: var(--music-list-item-mate-color);
+    height: auto;
+
+    :global(.van-button__text) {
+      display: flex;
+      align-items: center;
+
+      span {
+        line-height: 1;
+        padding-top: 2px;
+      }
+    }
+
+    >span {
+      display: inline-block;
+      line-height: 16px;
+      margin-top: 1px;
+    }
+  }
+
+  .favorite {
+    font-size: 16px;
+    margin-right: 5px;
+  }
+
+  .content {
+    flex: 1;
+    display: inline-grid;
+
+    >h4 {
+      color: var(--music-list-item-title-color);
+      font-size: 14px;
+      height: 20px;
+      line-height: 20px;
+      margin-top: 7px;
+      // word-break: break-all;
+      // text-overflow: ellipsis;
+    }
+
+    >p {
+      margin-top: 6px;
+      /* prettier-ignore */
+      font-size: 12PX;
+      color: var(--music-list-item-desc-color);
+      /* prettier-ignore */
+      line-height: 17PX;
+      /* prettier-ignore */
+      height: 51PX;
+    }
+  }
+}
+
+.footerBar {
+  padding: 12px 0;
+  display: flex;
+  justify-content: space-between;
+
+  >footer {
+    margin-top: 0;
+  }
+}
+
+.bgImg {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 265px;
+  object-fit: cover;
+  filter: blur(10px);
+  backdrop-filter: blur(22px);
+}
+
+.musicContent {
+  position: absolute;
+  top: 0;
+  height: 265px;
+  width: 100%;
+  padding-top: 55px;
+  z-index: 10;
+  background-color: rgba(0, 0, 0, 0.6);
+  backdrop-filter: blur(20px);
+  -webkit-backdrop-filter: blur(20px);
+}
+
+.bg {
+  position: relative;
+  height: 100%;
+  padding: 16px 16px 12px;
+  z-index: 11;
+}
+
+.alumWrap {
+  display: flex;
+  align-items: center;
+
+  .img {
+    width: 98px;
+    height: 98px;
+    flex-shrink: 0;
+    border-radius: 6px;
+    // overflow: hidden;
+    margin-right: 26px;
+  }
+
+  .alumTitle {
+    font-size: 16px;
+    font-weight: 600;
+    color: #fff;
+    padding-bottom: 8px;
+  }
+
+  .alumDes {
+    width: calc(100% - 129px);
+
+    .des {
+      color: #999;
+    }
+  }
+}
+
+.tags {
+  margin: 12px -2px 6px -2px;
+
+  .tag {
+    margin: 0 2px;
+    padding: 2px 6px;
+    color: #000;
+    // background-color: rgba(113, 138, 147, 1);
+    border-radius: 20px;
+    color: #FFFFFF;
+    border: 1px solid #FFFFFF;
+    font-size: 12px;
+  }
+}
+
+.alumCollect {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: 20px;
+  color: #999;
+  font-size: 14px;
+
+  .alumCollectItem {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 32%;
+    height: 36px;
+    font-size: 15px;
+    color: #fff;
+    background-color: rgba(255, 255, 255, 0.5);
+    border-radius: 99px;
+    font-weight: 600;
+
+    img {
+      width: 20px;
+      height: 20px;
+      margin-right: 6px;
+    }
+  }
+}
+
+.albumTips {
+  background: url('./charge_bg.png') no-repeat center;
+  background-size: contain;
+  // border-radius: 16px;
+  // opacity: 0.32;
+  padding: 10px 10px;
+  margin-top: 12px;
+  font-size: 13px;
+  font-weight: 500;
+  color: #5E3314;
+  line-height: 18px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  .albumPrice {
+    font-size: 14px;
+    font-weight: bold;
+    color: #FFC76C;
+    background: #262626;
+    border-radius: 13px;
+    padding: 4px 7px;
+
+  }
+}
+
+.alumnContainer {
+  position: relative;
+  padding: 0;
+  z-index: 12;
+
+  .alumnList {
+    padding: 16px 20px 0 12px;
+    border-radius: 18px;
+    background-color: #fff;
+    margin-bottom: 16px;
+  }
+
+  .alumnTitle {
+    display: flex;
+    align-items: center;
+    font-size: 16px;
+    font-weight: 600;
+    color: #2F384E;
+    line-height: 22px;
+
+    .iconMenu {
+      width: 22px;
+      height: 24px;
+      margin-right: 8px;
+    }
+
+    span {
+      font-size: 12px;
+      color: #808593;
+      padding-left: 3px;
+    }
+  }
+}
+
+.shareVip {
+  position: relative;
+  margin-top: 10px;
+  display: flex;
+  flex: 1;
+  align-items: center;
+  padding: 7px;
+  background: #ffffff;
+  border-radius: 10px;
+
+  .icon {
+    width: 72px;
+    height: 72px;
+    border-radius: 10px;
+  }
+
+  .info {
+    display: flex;
+    flex-direction: column;
+    margin-left: 6px;
+    flex: 1;
+    word-break: break-all;
+
+    >h4 {
+      color: var(--music-list-item-title-color);
+      font-size: 16px;
+      font-weight: 600;
+    }
+
+    >p {
+      color: var(--music-list-item-mate-color);
+      line-height: 17px;
+    }
+  }
+
+  .shareAlumCollect {
+    display: flex;
+    align-items: center;
+    color: #999;
+    font-size: 14px;
+
+    img {
+      display: inline-block;
+      width: 14px;
+      height: 14px;
+      margin-right: 6px;
+    }
+
+    span {
+      padding-top: 1px;
+    }
+
+    .right {
+      display: flex;
+      align-items: center;
+      margin-left: 26px;
+    }
+  }
+}
+
+.tagDiscount {
+  position: absolute;
+  top: -23px;
+  left: 15px;
+  padding: 0 10px;
+  height: 23px;
+  background: linear-gradient(180deg, #ffb635 0%, #ff4e18 100%);
+  border-radius: 8px 8px 0px 0px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #ffffff;
+  line-height: 24px;
+}
+
+.buttonDiscount {
+  position: absolute;
+  top: -23px;
+  right: 15px;
+  padding: 0 10px;
+  height: 23px;
+  background: linear-gradient(180deg, #ffb635 0%, #ff4e18 100%);
+  border-radius: 8px 8px 0px 0px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #ffffff;
+  line-height: 24px;
+}
+
+.shareMusicList {
+  margin-top: 12px;
+  box-shadow: 0px 2px 10px 0px rgba(0, 0, 0, 0.05);
+  padding: 0 10px;
+}
+
+.albumShare {
+  :global {
+    .btnGroup {
+      .van-button {
+        border: none;
+      }
+    }
+
+    .shareTeacherCustom {
+      background: linear-gradient(270deg, #BAFFE7 0%, #C0DCFF 100%);
+
+      &>div:first-child {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+    }
+
+    .downloadCustom {
+      &>div:last-child {
+        border-left-style: dashed;
+      }
+    }
+  }
+}

+ 511 - 0
src/tenant/music/album-detail/index.tsx

@@ -0,0 +1,511 @@
+import {
+  computed,
+  defineComponent,
+  nextTick,
+  onMounted,
+  reactive,
+  ref
+} from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import request from '@/helpers/request'
+import ColHeader from '@/components/col-header'
+import { postMessage } from '@/helpers/native-message'
+import { Button, Dialog, Icon, Image, Popup } from 'vant'
+import styles from './index.module.less'
+import { useRect } from '@vant/use'
+import { useEventListener, useWindowScroll } from '@vueuse/core'
+import { getRandomKey, musicBuy } from '../music'
+import { openDefaultWebView, state } from '@/state'
+import IconPan from './icon-pan.png'
+import oStart from './icon-hart.png'
+import iStart from './icon-hart-active.png'
+import Song from '../component/song'
+import ColResult from '@/components/col-result'
+import MusicGrid from '../component/music-grid'
+import { useEventTracking } from '@/helpers/hooks'
+import ColSticky from '@/components/col-sticky'
+import { moneyFormat } from '@/helpers/utils'
+import { orderStatus } from '@/views/order-detail/orderStatus'
+import iconShare from '../../images/icon-share.png'
+import ColShare from '@/components/col-share'
+import SongShare from '../component/song-share'
+import icon_music_list from './icon_music_list.png'
+import iconMenu from './icon-menu.png'
+import TheSticky from '@/components/the-sticky'
+
+const noop = () => {}
+
+export default defineComponent({
+  name: 'AlbumDetail',
+  props: {
+    onItemClick: {
+      type: Function,
+      default: noop
+    }
+  },
+  setup({ onItemClick }) {
+    localStorage.setItem('behaviorId', getRandomKey())
+    const router = useRouter()
+    const route = useRoute()
+    const params = reactive({
+      search: '',
+      relatedNum: 6, //相关专辑数
+      page: 1,
+      rows: 200
+    })
+    const albumDetail = ref<any>(null)
+    // const data = ref<any>(null)
+    const rows = ref<any[]>([])
+    const loading = ref(false)
+    const aId = Number(route.query.activityId) || 0
+    const studentActivityId = ref(aId)
+    // const finished = ref(false)
+    const isError = ref(false)
+    const favorited = ref(0)
+    const albumFavoriteCount = ref(0)
+    const background = ref<string>('rgba(55, 205, 177, 0)')
+    const color = ref<string>('#fff')
+
+    const FetchList = async (id?: any) => {
+      if (loading.value) {
+        return
+      }
+      loading.value = true
+      isError.value = false
+      try {
+        const res = await request.post('/music/album/detail', {
+          prefix:
+            state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student',
+          data: { id: id || route.params.id, ...params }
+        })
+        const { musicSheetList, ...rest } = res.data
+        rows.value = [...musicSheetList.rows]
+        const musicTagNames = rest?.musicTagNames
+          ? rest?.musicTagNames?.split(',')
+          : []
+        albumDetail.value = {
+          ...rest,
+          musicTagNames
+        }
+        favorited.value = rest.favorite
+        albumFavoriteCount.value = rest.albumFavoriteCount
+      } catch (error) {
+        isError.value = true
+      }
+      loading.value = false
+    }
+
+    const favoriteLoading = ref(false)
+
+    onMounted(() => {
+      FetchList()
+      useEventListener(document, 'scroll', evt => {
+        const { y } = useWindowScroll()
+        if (y.value > 20) {
+          background.value = `rgba(255, 255, 255)`
+          color.value = 'black'
+          postMessage({
+            api: 'backIconChange',
+            content: { iconStyle: 'black' }
+          })
+        } else {
+          background.value = 'transparent'
+          color.value = '#fff'
+          postMessage({
+            api: 'backIconChange',
+            content: { iconStyle: 'white' }
+          })
+        }
+      })
+
+      useEventTracking('专辑')
+    })
+
+    const toggleFavorite = async (id: number) => {
+      favoriteLoading.value = true
+      try {
+        await request.post('/music/album/favorite/' + id, {
+          prefix:
+            state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student'
+        })
+        favorited.value = favorited.value === 1 ? 0 : 1
+        albumFavoriteCount.value += favorited.value ? 1 : -1
+      } catch (error) {}
+      favoriteLoading.value = false
+    }
+
+    const onBuy = async () => {
+      const album = albumDetail.value
+      orderStatus.orderObject.orderType = 'ALBUM'
+      orderStatus.orderObject.orderName = album.albumName
+      orderStatus.orderObject.orderDesc = album.albumName
+      orderStatus.orderObject.actualPrice = album.albumPrice
+      orderStatus.orderObject.recomUserId = route.query.recomUserId || 0
+      orderStatus.orderObject.activityId = route.query.activityId || 0
+      orderStatus.orderObject.orderNo = ''
+      orderStatus.orderObject.orderList = [
+        {
+          orderType: 'ALBUM',
+          goodsName: album.albumName,
+          recomUserId: route.query.recomUserId || 0,
+          price: album.albumPrice,
+          ...album
+        }
+      ]
+
+      const res = await request.post('/api-student/userOrder/getPendingOrder', {
+        data: {
+          goodType: 'ALBUM',
+          bizId: album.id
+        }
+      })
+
+      const result = res.data
+      if (result) {
+        Dialog.confirm({
+          title: '提示',
+          message: '您有一个未支付的订单,是否继续支付?',
+          confirmButtonColor: '#269a93',
+          cancelButtonText: '取消订单',
+          confirmButtonText: '继续支付'
+        })
+          .then(async () => {
+            orderStatus.orderObject.orderNo = result.orderNo
+            orderStatus.orderObject.actualPrice = result.actualPrice
+            orderStatus.orderObject.discountPrice = result.discountPrice
+            routerTo()
+          })
+          .catch(() => {
+            Dialog.close()
+            // 只用取消订单,不用做其它处理
+            cancelPayment(result.orderNo)
+          })
+      } else {
+        routerTo()
+      }
+      // this.$router.push({
+      //   path: '/orderDetail',
+      //   query: {
+      //     orderType: 'VIP'
+      //   }
+      // })
+    }
+    const routerTo = () => {
+      const album = albumDetail.value
+      router.push({
+        path: '/orderDetail',
+        query: {
+          orderType: 'ALBUM',
+          album: album.id
+        }
+      })
+    }
+    const cancelPayment = async (orderNo: string) => {
+      try {
+        await request.post('/api-student/userOrder/orderCancel/v2', {
+          data: {
+            orderNo
+          }
+        })
+        // this.routerTo()
+      } catch {}
+    }
+
+    const shareStatus = ref<boolean>(false)
+    const shareUrl = ref<string>('')
+    const shareDiscount = ref<number>(0)
+    const onShare = async () => {
+      const userId = state.user.data.userId
+      const id = route.params.id
+      let activityId = 0
+      console.log(state.user, userId)
+      if (state.platformType === 'TEACHER') {
+        const res = await request.post('/api-teacher/open/vipProfit', {
+          data: {
+            bizId: id,
+            userId
+          }
+        })
+        // 如果有会员则显示
+        if (buyVip.value) {
+          activityId = res.data.activityId || 0
+          shareDiscount.value = res.data.discount || 0
+        }
+      }
+      shareUrl.value = `${location.origin}/teacher#/shareAblum?id=${id}&recomUserId=${userId}&activityId=${activityId}&userType=${state.platformType}`
+      console.log(shareUrl.value, 'shareUrl')
+      shareStatus.value = true
+    }
+
+    const buyVip = computed(() => {
+      const album = albumDetail.value?.musicPaymentTypes
+      return album && album.includes('VIP')
+    })
+
+    /** 分享曲谱列表, 最大数量2 */
+    const shareMusicList = computed(() => {
+      return rows.value.length > 2 ? rows.value.slice(0, 2) : rows.value
+    })
+    return () => {
+      return (
+        <div class={styles.detail}>
+          <TheSticky position="top">
+            <ColHeader
+              background={background.value}
+              border={false}
+              isFixed={false}
+              color={color.value}
+              backIconColor="white"
+            />
+          </TheSticky>
+          <img class={styles.bgImg} src={albumDetail.value?.albumCoverUrl} />
+          <div class={styles.musicContent}></div>
+          <div class={styles.bg}>
+            <div class={styles.alumWrap}>
+              <div class={styles.img}>
+                {/* {albumDetail.value?.paymentType === 'CHARGE' && (
+                  <span class={styles.albumType}>付费</span>
+                )} */}
+                <Image
+                  class={styles.image}
+                  width="100%"
+                  height="100%"
+                  fit="cover"
+                  src={albumDetail.value?.albumCoverUrl}
+                />
+              </div>
+              <div class={styles.alumDes}>
+                <div class={[styles.alumTitle, 'van-ellipsis']}>
+                  {albumDetail.value?.albumName}
+                </div>
+                <div
+                  class={[styles.des, 'van-multi-ellipsis--l2']}
+                  style={{
+                    height: '32px',
+                    lineHeight: '16px'
+                  }}
+                >
+                  {albumDetail.value?.albumDesc}
+                </div>
+                <div class={styles.tags}>
+                  {albumDetail.value?.musicTagNames?.map((tag: any) => (
+                    <span class={styles.tag}>{tag}</span>
+                  ))}
+                </div>
+              </div>
+            </div>
+            <div class={styles.alumCollect}>
+              <div class={styles.alumCollectItem} onClick={onShare}>
+                <Image src={iconShare} />
+                <span>分享</span>
+              </div>
+              <div
+                class={styles.alumCollectItem}
+                onClick={() =>
+                  router.push({
+                    path: '/music-album'
+                  })
+                }
+              >
+                <img src={IconPan} />
+                <span>相关专辑</span>
+              </div>
+              <div
+                class={styles.alumCollectItem}
+                onClick={() => toggleFavorite(albumDetail.value?.id)}
+              >
+                <img src={favorited.value ? iStart : oStart} />
+                <span>{albumFavoriteCount.value}</span>
+              </div>
+            </div>
+
+            {albumDetail.value?.paymentType === 'CHARGE' &&
+              albumDetail.value?.orderStatus !== 'PAID' && (
+                <div class={styles.albumTips}>
+                  <span>开通会员或购买专辑,即可自由练习该专辑</span>
+                  <span class={styles.albumPrice}>
+                    ¥{moneyFormat(albumDetail.value?.albumPrice)}
+                  </span>
+                </div>
+              )}
+          </div>
+          <div class={styles.alumnContainer}>
+            <div class={styles.alumnList}>
+              {/* <Title title="曲目列表" isMore={false} /> */}
+              <div class={styles.alumnTitle}>
+                <img src={iconMenu} class={styles.iconMenu} />
+                曲目列表{' '}
+                <span>({albumDetail.value?.musicSheetCount || 0})</span>
+              </div>
+              <Song
+                showNumber
+                list={rows.value}
+                onDetail={(item: any) => {
+                  if (onItemClick === noop || !onItemClick) {
+                    const url =
+                      location.origin +
+                      location.pathname +
+                      '#/music-detail?id=' +
+                      item.id +
+                      '&albumId=' +
+                      route.params.id
+                    openDefaultWebView(url, () => {
+                      router.push({
+                        path: '/music-detail',
+                        query: {
+                          id: item.id,
+                          albumId: route.params.id
+                        }
+                      })
+                    })
+                  } else {
+                    onItemClick(item)
+                  }
+                }}
+              />
+
+              {rows.value && rows.value.length <= 0 && (
+                <ColResult btnStatus={false} tips="暂无曲目" />
+              )}
+            </div>
+
+            {/* {albumDetail.value?.relatedMusicAlbum &&
+              albumDetail.value?.relatedMusicAlbum.length > 0 && (
+                <>
+                  <Title
+                    title="相关专辑"
+                    onMore={() => {
+                      router.push({
+                        path: '/music-album'
+                      })
+                    }}
+                  />
+
+                  <MusicGrid
+                    list={albumDetail.value?.relatedMusicAlbum}
+                    onGoto={(n: any) => {
+                      router
+                        .push({
+                          name: 'music-album-detail',
+                          params: {
+                            id: n.id
+                          }
+                        })
+                        .then(() => {
+                          FetchList(n.id)
+                          window.scrollTo(0, 0)
+                        })
+                    }}
+                  />
+                </>
+              )} */}
+          </div>
+
+          {/* 判断是否是收费 是否是已经购买 */}
+          {albumDetail.value?.paymentType === 'CHARGE' &&
+            albumDetail.value?.orderStatus !== 'PAID' && (
+              <ColSticky position="bottom" background="white">
+                <div
+                  class={[
+                    'btnGroup',
+                    buyVip.value && !state.user.data.isVip && 'btnMore'
+                  ]}
+                  style={{ paddingTop: '12px' }}
+                >
+                  <Button
+                    block
+                    round
+                    type="primary"
+                    style={{ fontSize: '16px' }}
+                    onClick={onBuy}
+                    color="linear-gradient(270deg, #FF204B 0%, #FE5B71 100%)"
+                  >
+                    购买专辑
+                  </Button>
+                  {buyVip.value && !state.user.data.isVip && (
+                    <Button
+                      block
+                      round
+                      type="primary"
+                      style={{ fontSize: '16px' }}
+                      color="linear-gradient(270deg, #FF204B 0%, #FE5B71 100%)"
+                      onClick={() => {
+                        router.push({
+                          path: '/memberCenter',
+                          query: {
+                            ...route.query
+                          }
+                        })
+                      }}
+                    >
+                      {studentActivityId.value > 0 && (
+                        <div class={[styles.buttonDiscount]}>专属优惠</div>
+                      )}
+                      开通会员
+                    </Button>
+                  )}
+                </div>
+              </ColSticky>
+            )}
+
+          <Popup
+            v-model:show={shareStatus.value}
+            style={{ background: 'transparent' }}
+            class={styles.albumShare}
+          >
+            <ColShare
+              type="tenant"
+              teacherId={state.user.data.userId}
+              shareUrl={shareUrl.value}
+              shareType="album"
+              shareLength={1}
+            >
+              <div class={styles.shareVip}>
+                {shareDiscount.value === 1 && (
+                  <div class={styles.tagDiscount}>专属优惠</div>
+                )}
+
+                <img
+                  class={styles.icon}
+                  crossorigin="anonymous"
+                  src={
+                    albumDetail.value?.albumCoverUrl +
+                    `@base@tag=imgScale&h=144&w=144&m=1?t=${+new Date()}`
+                  }
+                />
+                <div class={styles.info}>
+                  <h4 class="van-multi-ellipsis--l2">
+                    {albumDetail.value?.albumName}
+                  </h4>
+                  <p
+                    class={['van-multi-ellipsis--l3']}
+                    style={{
+                      lineHeight: '16px',
+                      margin: '5px 0 10px 0'
+                    }}
+                  >
+                    {albumDetail.value?.albumDesc}
+                  </p>
+                  <div class={styles.shareAlumCollect}>
+                    <img src={icon_music_list} />
+                    <span>
+                      共
+                      <span style="color: var(--van-primary-color);">
+                        {albumDetail.value?.musicSheetCount}
+                      </span>
+                      首曲目
+                    </span>
+                  </div>
+                </div>
+              </div>
+
+              <div class={[styles.shareVip, styles.shareMusicList]}>
+                <SongShare list={shareMusicList.value} />
+              </div>
+            </ColShare>
+          </Popup>
+        </div>
+      )
+    }
+  }
+})

二进制
src/tenant/music/album-detail/oStart.png


二进制
src/tenant/music/album-detail/pan.png


+ 19 - 0
src/tenant/music/album/count.svg

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="15px" height="15px" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>曲目数量</title>
+    <g id="页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="专辑" transform="translate(-136.000000, -236.000000)" fill="#DADADA" fill-rule="nonzero">
+            <g id="编组-18" transform="translate(14.000000, 151.000000)">
+                <g id="编组-9" transform="translate(10.000000, 10.000000)">
+                    <g id="编组-2" transform="translate(112.000000, 4.000000)">
+                        <g id="编组-8" transform="translate(0.000000, 70.000000)">
+                            <g id="曲目数量" transform="translate(0.000000, 1.000000)">
+                                <path d="M7.5,13.7325 C4.0600867,13.7276789 1.27288113,10.9399142 1.26875,7.5 C1.27356953,4.06057395 4.06057395,1.27356953 7.5,1.26875 C10.9399963,1.27219516 13.7278048,4.06000369 13.73125,7.5 C13.7264305,10.9394261 10.9394261,13.7264305 7.5,13.73125 L7.5,13.7325 Z M7.5,0 C3.35957804,0.00413422372 0.00413422372,3.35957804 0,7.5 C0.00482172375,11.6401368 3.35986324,14.9951783 7.5,15 C11.6401368,14.9951783 14.9951783,11.6401368 15,7.5 C15,3.36375 11.635,0 7.5,0 Z M5.65,2.3825 C4.01426316,2.97269069 2.75969986,4.30996224 2.275,5.98 C2.25875001,6.04 2.05625001,6.6825 2.72375,6.81375 C3.39,6.94375 3.51375,6.26875 3.56125,6.13 C3.97801767,4.92223188 4.92787577,3.97375706 6.13625,3.55875 C6.245,3.52125 6.85374999,3.22875 6.64125,2.6725 C6.42875,2.11625001 5.75125,2.3475 5.6525,2.38375 L5.6525,2.3825 L5.65,2.3825 Z M7.5,8.695 C7.0636448,8.70956018 6.65401361,8.48523833 6.43126245,8.10973895 C6.20851129,7.73423958 6.20802302,7.26720878 6.42998854,6.89124446 C6.65195405,6.51528014 7.0611153,6.29010226 7.4975,6.30375 C7.92487896,6.30330342 8.32003165,6.53089472 8.53410789,6.90079247 C8.74818412,7.27069021 8.74866062,7.72669814 8.53535789,8.09704247 C8.32205516,8.4673868 7.92737896,8.69580342 7.5,8.69625 L7.5,8.695 Z M7.5,5.0325 C6.13809049,5.0325 5.03398032,6.13642406 5.03375031,7.49833355 C5.03352037,8.86024304 6.13725766,9.96453975 7.49916709,9.96500001 C8.86107653,9.96545999 9.96555973,8.86190934 9.96625,7.5 C9.96418334,6.13898713 8.86101375,5.03637667 7.5,5.035 L7.5,5.0325 Z" id="形状"></path>
+                            </g>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
src/tenant/music/album/favorite.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <g id="页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="曲目" transform="translate(-325.000000, -331.000000)" fill-rule="nonzero" stroke="#DEDEDE" stroke-width="1.5">
+            <g id="编组-5备份" transform="translate(14.000000, 151.000000)">
+                <g id="专辑曲目备份" transform="translate(0.000000, 109.000000)">
+                    <g id="收藏(未收藏)" transform="translate(311.000000, 71.000000)">
+                        <path d="M8.59307642,1.86900124 C9.15691608,1.75 9.30638755,1.79544827 9.43239448,1.87364717 C9.55085979,1.9471658 9.65028194,2.05023484 9.70454832,2.17756979 L9.70454832,2.17756979 L11.5265134,5.78670141 L15.5821251,6.47580081 C15.7327103,6.4957182 15.8715474,6.55831854 15.9824664,6.65280684 C16.0849532,6.740112 16.1636145,6.85506007 16.2025537,6.98876702 C16.2041817,6.99435719 16.2057335,6.99995697 16.2072174,7.00554243 L16.2130095,7.02180617 C16.2572718,7.15119891 16.2606889,7.28682761 16.2287543,7.4144129 C16.1951838,7.54853363 16.1231674,7.67349127 16.0200313,7.77616377 L16.0200313,7.77616377 L13.0847628,10.4774999 L13.826464,14.3769127 C13.8521735,14.5093325 13.8369316,14.6425107 13.7880718,14.7637018 C13.7359294,14.8930352 13.6463939,15.0084051 13.5288921,15.0992482 C13.3886507,15.193897 13.2263619,15.25 13.0588435,15.25 C12.9413454,15.25 12.8084061,15.2143491 12.6878668,15.150301 L12.6878668,15.150301 L9.00337267,13.3551448 L5.2933603,15.1503249 C5.18086627,15.2108078 5.0557276,15.242485 4.92250872,15.242485 C4.76083064,15.242485 4.60111495,15.1978475 4.47200526,15.1025395 C4.35087485,15.0135302 4.25990291,14.8923676 4.20576979,14.7584653 C4.15522512,14.6334393 4.13616929,14.496192 4.16216826,14.3640909 L4.16216826,14.3640909 L4.93063225,10.46854 L1.98567829,7.79965757 C1.88384433,7.69519884 1.81246621,7.57031448 1.77665699,7.43600596 C1.74206905,7.30627808 1.74090102,7.16826338 1.77509146,7.03322497 C1.82858866,6.88982379 1.91305342,6.76776107 2.02227189,6.67700458 C2.13564433,6.58279631 2.27495623,6.52344169 2.42708865,6.50650998 L2.42708865,6.50650998 L6.48779769,5.78329312 L8.29877184,2.18764621 C8.36633803,2.05403159 8.47006747,1.94509508 8.59307642,1.86900124 Z" id="路径"></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 13 - 0
src/tenant/music/album/favorited.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <g id="页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="专辑详情" transform="translate(-299.000000, -245.000000)" fill="#FFC459" fill-rule="nonzero">
+            <g id="编组-9" transform="translate(276.000000, 245.000000)">
+                <g id="已收藏" transform="translate(23.000000, 0.000000)">
+                    <path d="M13.0588435,16 C12.8351907,16 12.5806892,15.9398798 12.3493243,15.8196393 L9.00224436,14.1888778 L5.63202796,15.8196393 C5.41608732,15.9323647 5.17701019,15.992485 4.92250872,15.992485 C4.59859776,15.992485 4.28239897,15.8947896 4.0278975,15.7069138 C3.55745539,15.3612224 3.31837826,14.7675351 3.42634858,14.2189379 L4.11273132,10.739479 L1.4674585,8.34218437 C1.05871372,7.93637275 0.904470402,7.35771543 1.05871372,6.80911824 L1.06642588,6.79408818 C1.25923002,6.23046092 1.73738429,5.83967936 2.31579672,5.76452906 L5.98678758,5.11072144 L7.62947887,1.8491984 C7.89169251,1.33066132 8.4315441,1 9.00224436,1 C9.59608112,1 10.151357,1.34569138 10.382722,1.85671343 L12.0254133,5.11072144 L15.6964042,5.73446894 C16.2748166,5.81713427 16.760683,6.22294589 16.9226385,6.77905812 C17.1077305,7.32014028 16.9534872,7.91382766 16.5370302,8.31963928 L16.5293181,8.32715431 L13.8994696,10.746994 L14.5627158,14.2339679 C14.6706861,14.7900802 14.4393212,15.3537074 13.9688791,15.7069138 C13.6989533,15.8947896 13.3827545,16 13.0588435,16 Z" id="路径"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 36 - 0
src/tenant/music/album/footer.tsx

@@ -0,0 +1,36 @@
+import { Icon } from 'vant'
+import { defineComponent, toRefs } from 'vue'
+import CountIcon from './count.svg'
+import FavoriteIcon from './favorite.svg'
+import styles from './item.module.less'
+
+export default defineComponent({
+  name: 'AlbumFooter',
+  props: {
+    musicSheetCount: {
+      type: Number,
+      default: 0
+    },
+    albumFavoriteCount: {
+      type: Number,
+      default: 0
+    }
+  },
+  setup(props) {
+    const { musicSheetCount, albumFavoriteCount } = toRefs(props)
+    return () => {
+      return (
+        <footer class={styles.footer}>
+          <div>
+            <Icon class={styles.icon} name={CountIcon} />
+            <span>{musicSheetCount.value}首</span>
+          </div>
+          <div>
+            <Icon class={styles.icon} name={FavoriteIcon} />
+            <span>{albumFavoriteCount.value}人收藏</span>
+          </div>
+        </footer>
+      )
+    }
+  }
+})

+ 18 - 0
src/tenant/music/album/icon_share.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="曲目(老师分享)" transform="translate(-295.000000, -222.000000)">
+            <g id="编组-5备份" transform="translate(14.000000, 151.000000)">
+                <g id="分享(专辑)" transform="translate(281.000000, 71.000000)">
+                    <rect id="矩形" x="0" y="0" width="18" height="18"></rect>
+                    <g id="编组-5" transform="translate(1.960261, 2.294652)" stroke="#fff" stroke-linecap="round" stroke-width="1.4">
+                        <path d="M7,0.0980762114 C3.13400675,0.0980762114 0,3.23208296 0,7.09807621 C0,10.9640695 3.13400675,14.0980762 7,14.0980762 C10.8659932,14.0980762 14,10.9640695 14,7.09807621" id="路径"></path>
+                        <path d="M13.0553063,2.25732549 C9.74159779,2.25732549 7.05530629,4.94361699 7.05530629,8.25732549" id="路径"></path>
+                        <polyline id="路径" stroke-linejoin="round" transform="translate(11.366025, 2.732051) rotate(-330.000000) translate(-11.366025, -2.732051) " points="9.3660254 0.732050808 13.3660254 0.732050808 13.3660254 4.73205081"></polyline>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 18 - 0
src/tenant/music/album/icon_share2.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="曲目(老师分享)" transform="translate(-295.000000, -222.000000)">
+            <g id="编组-5备份" transform="translate(14.000000, 151.000000)">
+                <g id="分享(专辑)" transform="translate(281.000000, 71.000000)">
+                    <rect id="矩形" x="0" y="0" width="18" height="18"></rect>
+                    <g id="编组-5" transform="translate(1.960261, 2.294652)" stroke="#666666" stroke-linecap="round" stroke-width="1.4">
+                        <path d="M7,0.0980762114 C3.13400675,0.0980762114 0,3.23208296 0,7.09807621 C0,10.9640695 3.13400675,14.0980762 7,14.0980762 C10.8659932,14.0980762 14,10.9640695 14,7.09807621" id="路径"></path>
+                        <path d="M13.0553063,2.25732549 C9.74159779,2.25732549 7.05530629,4.94361699 7.05530629,8.25732549" id="路径"></path>
+                        <polyline id="路径" stroke-linejoin="round" transform="translate(11.366025, 2.732051) rotate(-330.000000) translate(-11.366025, -2.732051) " points="9.3660254 0.732050808 13.3660254 0.732050808 13.3660254 4.73205081"></polyline>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 124 - 0
src/tenant/music/album/index.module.less

@@ -0,0 +1,124 @@
+.memberHeader {
+  position: relative;
+  z-index: 1;
+  background: url('@/tenant/images/bg-image.png') no-repeat top center;
+  background-size: 100% 265px;
+}
+
+.headerImg {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 265px;
+  object-fit: cover;
+  filter: blur(10px);
+}
+
+.headerContent {
+  :global {
+    .van-search__content {
+      background: rgba(255, 255, 255, 0.5) !important;
+
+      input::placeholder {
+        color: rgba(0, 0, 0, 0.4) !important;
+      }
+
+      input {
+        color: rgba(0, 0, 0, 0.4) !important;
+      }
+
+      .van-field__clear {
+        color: rgba(0, 0, 0, 0.4) !important;
+      }
+    }
+  }
+}
+
+
+.sticky {
+  :global(.van-sticky--fixed) {
+    box-shadow: 10px 10px 10px var(--box-shadow-color);
+  }
+
+
+}
+
+.label {
+  margin-right: 8px;
+  font-size: 14px;
+
+  :global {
+
+    .van-list__loading,
+    .van-list__finished-text,
+    .van-list__error-text {
+      width: 100%;
+    }
+
+    .iconfont-down {
+      margin-left: 4px;
+    }
+  }
+}
+
+.musicGrid {
+  margin: 16px 12px;
+}
+
+.tagTabs {
+  :global {
+    .van-tabs__nav {
+      background-color: transparent;
+      padding: 0;
+      margin: 0 15px;
+    }
+
+    .van-tab {
+      font-size: 16px;
+      font-weight: bold;
+    }
+
+    .van-tab--shrink {
+      padding: 0;
+      margin: 10px 0;
+      display: inline-block;
+      font-size: 14px;
+      background: transparent;
+      border-radius: 14px;
+      line-height: 26px;
+      padding: 0 12px;
+      color: rgba(0, 0, 0, 0.4);
+    }
+
+    .van-tab--active {
+      // &::after {
+      //   content: ' ';
+      //   display: inline-block;
+      //   width: 96%;
+      //   position: absolute;
+      //   height: 7px;
+      //   background: rgba(45, 199, 170, 0.5);
+      //   border-radius: 4px;
+      //   bottom: 0;
+      //   left: 2%;
+      //   transition: all ease 0.3s;
+      // }
+      background: #FF699E;
+
+      color: #FFFFFF;
+
+      .van-tab__text {
+        z-index: 1;
+      }
+    }
+
+    .van-tabs__line {
+      height: 0;
+      // bottom: 30px;
+      // height: 7px;
+      // background: rgba(45, 199, 170, 0.5);
+      // border-radius: 4px;
+    }
+  }
+}

+ 383 - 0
src/tenant/music/album/index.tsx

@@ -0,0 +1,383 @@
+import { defineComponent, reactive, ref } from 'vue'
+import { Sticky, List, Popup, Icon, Tabs, Tab, Image } from 'vant'
+import Search from '@/components/col-search'
+import request from '@/helpers/request'
+import Item from './item'
+import SelectTag from '../search/select-tag'
+import { useRoute, useRouter } from 'vue-router'
+import ColResult from '@/components/col-result'
+import styles from './index.module.less'
+import { state as baseState } from '@/state'
+import SelectSubject from '../search/select-subject'
+import { SubjectEnum, useSubjectId } from '@/helpers/hooks'
+import MusicGrid from '../component/music-grid'
+import { useAsyncState } from '@vueuse/core'
+import ColHeader from '@/components/col-header'
+import TheSticky from '@/components/the-sticky'
+import bgImage from '@/tenant/images/bg-image.png'
+
+export default defineComponent({
+  name: 'Album',
+  props: {
+    hideSearch: {
+      type: Boolean,
+      default: false
+    },
+    defauleParams: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  setup({ hideSearch, defauleParams }, { expose }) {
+    const { isLoading, state } = useAsyncState(
+      request(baseState.platformApi + '/MusicTag/tree', {
+        params: {
+          type: 'ALBUM'
+        }
+      }),
+      null
+    )
+
+    const teacherDetaultSubject = ref({
+      id: '',
+      name: ''
+    })
+    if (baseState.platformType === 'TEACHER') {
+      const users = baseState.user.data
+      teacherDetaultSubject.value = {
+        name: users.defaultSubjectName || '全部声部',
+        id: users.defaultSubject || ''
+      }
+    } else {
+      const subjects: any = useSubjectId(SubjectEnum.SEARCH)
+      // 判断是否已有数据
+      if (!subjects.id) {
+        const users = baseState.user.data
+        const subjectId = users.subjectId
+          ? Number(users.subjectId.split(',')[0])
+          : ''
+        const subjectName = users.subjectName
+          ? users.subjectName.split(',')[0]
+          : ''
+        if (subjectId) {
+          useSubjectId(
+            SubjectEnum.SEARCH,
+            JSON.stringify({
+              id: subjectId,
+              name: subjectName
+            }),
+            'set'
+          )
+        }
+      }
+    }
+
+    const router = useRouter()
+    const route = useRoute()
+    const tempParams: any = {}
+    if (baseState.version) {
+      tempParams.version = baseState.version || '' // 处理ios审核版本
+      tempParams.platform =
+        baseState.platformType === 'STUDENT' ? 'ios-student' : 'ios-teacher'
+    }
+    tempParams.myself = false
+    if (!hideSearch) {
+      if (baseState.platformType === 'TEACHER') {
+        tempParams.subjectIds = teacherDetaultSubject.value.id
+      } else {
+        const getSubject: any = useSubjectId(SubjectEnum.SEARCH)
+        tempParams.subjectIds = getSubject.id
+      }
+    }
+    const params = reactive({
+      search: (route.query.search as string) || '',
+      albumTagIds: route.query.tagids || '',
+      page: 1,
+      ...defauleParams,
+      ...tempParams
+    })
+    const data = ref<any>(null)
+    const loading = ref(false)
+    const finished = ref(false)
+    const isError = ref(false)
+    const tagVisibility = ref(false)
+
+    const onSearch = (value: string) => {
+      params.page = 1
+      params.search = value
+      data.value = null
+      FetchList()
+    }
+
+    const FetchList = async () => {
+      if (loading.value) {
+        return
+      }
+      loading.value = true
+      isError.value = false
+      try {
+        const res = await request.post('/music/album/list', {
+          prefix:
+            baseState.platformType === 'TEACHER'
+              ? '/api-teacher'
+              : '/api-student',
+          data: {
+            ...params,
+            idAndName: params.search
+          }
+        })
+        if (data.value) {
+          const result = (data.value?.rows || []).concat(res.data.rows || [])
+          data.value.rows = result
+        }
+        data.value = data.value || res.data
+        params.page = res.data.pageNo + 1
+        finished.value = res.data.pageNo >= res.data.totalPage
+      } catch (error) {
+        isError.value = true
+      }
+      loading.value = false
+    }
+
+    // 设置默认声部
+    const setDefaultSubject = async (subjectId: any) => {
+      try {
+        await request.post('/api-teacher/teacher/defaultSubject', {
+          params: {
+            subjectId
+          }
+        })
+      } catch {
+        //
+      }
+    }
+
+    const onComfirm = tags => {
+      const d = Object.values(tags).flat().filter(Boolean).join(',')
+      params.albumTagIds = d
+      params.page = 1
+      data.value = null
+      FetchList()
+      tagVisibility.value = false
+    }
+
+    const onComfirmSubject = item => {
+      params.page = 1
+      params.subjectIds = item.id
+
+      data.value = null
+      if (baseState.platformType === 'TEACHER') {
+        teacherDetaultSubject.value = {
+          name: item.name,
+          id: item.id
+        }
+        setDefaultSubject(item.id)
+      } else {
+        subject.id = item.id
+        subject.name = item.name
+        useSubjectId(
+          SubjectEnum.SEARCH,
+          JSON.stringify({
+            id: item.id,
+            name: item.name
+          }),
+          'set'
+        )
+      }
+      FetchList()
+      subject.show = false
+    }
+
+    expose({
+      onSearch,
+      onComfirm,
+      onComfirmSubject
+    })
+
+    const getSubject: any = useSubjectId(SubjectEnum.SEARCH)
+    const subject = reactive({
+      show: false,
+      name: getSubject.name || '全部声部',
+      id: getSubject.id || ''
+    })
+
+    return () => {
+      const tagList = ((state.value && state.value.data) as any) || []
+      return (
+        <div>
+          <List
+            loading={loading.value}
+            finished={finished.value}
+            finished-text={
+              data.value && data.value.rows.length ? '没有更多了' : ''
+            }
+            onLoad={FetchList}
+            error={isError.value}
+          >
+            {!hideSearch && (
+              <>
+                <TheSticky position="top">
+                  <ColHeader
+                    class={styles.memberHeader}
+                    background="transparent"
+                    backIconColor="white"
+                    border={false}
+                    isFixed={false}
+                    color="#131415"
+                    v-slots={{
+                      default: () => (
+                        <div class={styles.headerContent}>
+                          <Search
+                            type="tenant"
+                            modelValue={params.search}
+                            onSearch={onSearch}
+                            placeholder="请输入专辑名称 "
+                            background="transparent"
+                            v-slots={{
+                              left: () => (
+                                <div
+                                  class={styles.label}
+                                  onClick={() => (subject.show = true)}
+                                >
+                                  {baseState.platformType === 'TEACHER'
+                                    ? teacherDetaultSubject.value.name
+                                    : subject.name}
+                                  <Icon
+                                    classPrefix="iconfont"
+                                    name="down"
+                                    size={12}
+                                    color="#333"
+                                  />
+                                </div>
+                              )
+                            }}
+                          />
+                          <Tabs
+                            shrink
+                            class={styles.tagTabs}
+                            lineHeight={0}
+                            onClick-tab={(obj: any) => {
+                              params.albumTagIds = obj.name
+                              data.value = null
+                              params.page = 1
+                              FetchList()
+                            }}
+                          >
+                            <Tab title="全部" name=""></Tab>
+                            {tagList.map((tag: any) => (
+                              <Tab title={tag.name} name={tag.id}></Tab>
+                            ))}
+                          </Tabs>
+                        </div>
+                      )
+                    }}
+                  />
+                </TheSticky>
+                <Image class={styles.headerImg} src={bgImage} />
+              </>
+              // <Sticky class={styles.sticky}>
+              //   <Search
+              //     modelValue={params.search}
+              //     onSearch={onSearch}
+              //     placeholder="请输入专辑名称 "
+              //     v-slots={{
+              //       left: () => (
+              //         <div
+              //           class={styles.label}
+              //           onClick={() => (subject.show = true)}
+              //         >
+              //           {baseState.platformType === 'TEACHER'
+              //             ? teacherDetaultSubject.value.name
+              //             : subject.name}
+              //           <Icon
+              //             classPrefix="iconfont"
+              //             name="down"
+              //             size={12}
+              //             color="#333"
+              //           />
+              //         </div>
+              //       )
+              //     }}
+              //   />
+              //   <Tabs
+              //     shrink
+              //     class={styles.tagTabs}
+              //     lineHeight={0}
+              //     onClick-tab={(obj: any) => {
+              //       params.albumTagIds = obj.name
+              //       data.value = null
+              //       params.page = 1
+              //       FetchList()
+              //     }}
+              //   >
+              //     <Tab title="全部" name=""></Tab>
+              //     {tagList.map((tag: any) => (
+              //       <Tab title={tag.name} name={tag.id}></Tab>
+              //     ))}
+              //   </Tabs>
+              // </Sticky>
+            )}
+            {data.value && data.value.rows.length ? (
+              <div class={styles.musicGrid}>
+                <MusicGrid
+                  list={data.value.rows}
+                  onGoto={(n: any) => {
+                    router.push({
+                      name: 'music-album-detail',
+                      params: {
+                        id: n.id
+                      }
+                    })
+                  }}
+                />
+              </div>
+            ) : (
+              // data.value.rows.map(item => <Item data={item} />)
+              !loading.value && (
+                <ColResult
+                  tips="暂无专辑"
+                  classImgSize="SMALL"
+                  btnStatus={false}
+                />
+              )
+            )}
+          </List>
+          {/* <Popup
+            show={tagVisibility.value}
+            round
+            closeable
+            position="bottom"
+            style={{ height: '60%' }}
+            teleport="body"
+            onUpdate:show={val => (tagVisibility.value = val)}
+          >
+            <SelectTag
+              defaultValue={route.query.tagids as string}
+              onConfirm={onComfirm}
+              onCancel={() => {}}
+            />
+          </Popup> */}
+          <Popup
+            show={subject.show}
+            position="bottom"
+            round
+            closeable
+            safe-area-inset-bottom
+            onClose={() => (subject.show = false)}
+            onClosed={() => (subject.show = false)}
+          >
+            <SelectSubject
+              type="ALBUM"
+              searchParams={
+                baseState.platformType === 'TEACHER'
+                  ? teacherDetaultSubject.value
+                  : subject
+              }
+              onComfirm={onComfirmSubject}
+            />
+          </Popup>
+        </div>
+      )
+    }
+  }
+})

+ 83 - 0
src/tenant/music/album/item.module.less

@@ -0,0 +1,83 @@
+.album {
+  margin: 12px 14px;
+  padding: 10px;
+  background-color: var(--music-list-item-background-color);
+  border-radius: 10px;
+  display: flex;
+  position: relative;
+  .albumType {
+    position: absolute;
+    left: 10px;
+    top: 10px;
+    background: linear-gradient(180deg, #ff8900 0%, #ff5100 100%);
+    box-shadow: 0px 1px 2px 0px rgba(150, 13, 0, 0.11);
+    border-radius: 10px 0px 10px 0px;
+    font-size: 12px;
+    padding: 0 6px;
+    line-height: 20px;
+    color: #ffffff;
+  }
+  .img {
+    width: 94px;
+    height: 94px;
+    margin-right: 18px;
+    position: relative;
+    > img,
+    > div {
+      position: absolute;
+      border-radius: 10px;
+      overflow: hidden;
+    }
+    &::before {
+      content: '';
+      width: 80px;
+      height: 80px;
+      border-radius: 9px;
+      background-color: var(--music-list-item-background-color);
+      box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.08);
+      position: absolute;
+      right: -6px;
+      top: 8px;
+    }
+  }
+  .content {
+    flex: 1;
+    display: inline-grid;
+    > h4 {
+      color: var(--music-list-item-title-color);
+      font-size: 14px;
+      height: 20px;
+      line-height: 20px;
+    }
+    > p {
+      margin-top: 6px;
+      /* prettier-ignore */
+      font-size: 12PX;
+      color: var(--music-list-item-desc-color);
+      /* prettier-ignore */
+      line-height: 17PX;
+      /* prettier-ignore */
+      height: 51PX;
+    }
+  }
+}
+
+.footer {
+  margin-top: 11px;
+  display: flex;
+  > div {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 12px;
+    color: var(--music-list-item-mate-color);
+    margin-right: 18px;
+    .icon {
+      margin-right: 5px;
+    }
+    span {
+      display: block;
+      margin-top: 1px;
+    }
+  }
+}

+ 38 - 0
src/tenant/music/album/item.tsx

@@ -0,0 +1,38 @@
+import { Image } from 'vant'
+import { defineComponent } from 'vue'
+import { useRouter } from 'vue-router'
+import Footer from './footer'
+import styles from './item.module.less'
+
+export default defineComponent({
+  name: 'AlbumItem',
+  props: {
+    data: {
+      type: Object,
+      default: {}
+    }
+  },
+  setup({ data }) {
+    const router = useRouter()
+    return () => (
+      <div
+        class={styles.album}
+        onClick={() => router.push('/music-album-detail/' + data.id)}
+      >
+        <Image class={styles.img} src={data.albumCoverUrl} />
+        {data.paymentType === 'CHARGE' && (
+          <span class={styles.albumType}>付费</span>
+        )}
+
+        <div class={styles.content}>
+          <h4 class="van-ellipsis">{data.albumName}</h4>
+          <p class="van-multi-ellipsis--l3">{data.albumDesc}</p>
+          <Footer
+            musicSheetCount={data.musicSheetCount}
+            albumFavoriteCount={data.albumFavoriteCount}
+          />
+        </div>
+      </div>
+    )
+  }
+})

+ 0 - 0
src/tenant/music/component/collection/index.module.less


+ 13 - 0
src/tenant/music/component/collection/index.tsx

@@ -0,0 +1,13 @@
+import { Cell, CellGroup } from 'vant'
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  name: 'Collection',
+  setup() {
+    return () => (
+      <CellGroup>
+        <Cell title={'收藏曲目'} />
+      </CellGroup>
+    )
+  }
+})

二进制
src/tenant/music/component/images/collection.png


二进制
src/tenant/music/component/images/collection_active.png


二进制
src/tenant/music/component/images/icon-play.png


二进制
src/tenant/music/component/images/icon-xin.png


二进制
src/tenant/music/component/images/icon_ai.png


二进制
src/tenant/music/component/images/icon_album.png


二进制
src/tenant/music/component/images/icon_album_active.png


二进制
src/tenant/music/component/images/icon_author.png


二进制
src/tenant/music/component/images/icon_download.png


二进制
src/tenant/music/component/images/icon_exquisite.png


二进制
src/tenant/music/component/images/icon_music_active.png


二进制
src/tenant/music/component/images/icon_share.png


二进制
src/tenant/music/component/images/icon_uploader.png


+ 71 - 0
src/tenant/music/component/music-grid/index.module.less

@@ -0,0 +1,71 @@
+.theMusicGrid {
+  :global {
+    .van-grid {
+      margin: 0 -4px;
+    }
+    .van-grid-item {
+      width: calc(100% / 3);
+    }
+    .van-grid-item__content {
+      display: block;
+      padding: 0 8px;
+      background-color: transparent;
+    }
+  }
+  .item {
+    margin-bottom: 14px;
+    .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: 104px;
+    // height: calc((100vw - 28px) / 3);
+    border-radius: 10px;
+    overflow: hidden;
+    margin-bottom: 6px;
+    .albumType {
+      position: absolute;
+      left: 0;
+      top: 0;
+      background: linear-gradient(180deg, #ff8900 0%, #ff5100 100%);
+      box-shadow: 0px 1px 2px 0px rgba(150, 13, 0, 0.11);
+      border-radius: 10px 0px 10px 0px;
+      font-size: 12px;
+      padding: 0 6px;
+      line-height: 20px;
+      color: #ffffff;
+      z-index: 9;
+    }
+    .model {
+      position: absolute;
+      left: 4px;
+      bottom: 4px;
+      background: rgba(67, 67, 67, 0.6);
+      backdrop-filter: blur(20px);
+      -webkit-backdrop-filter: blur(20px);
+      display: flex;
+      align-items: center;
+      padding: 4px 6px;
+      border-radius: 20px;
+      font-size: 12px;
+      color: #fff;
+      transform: scale(0.9);
+    }
+    .num {
+      margin-left: 3px;
+    }
+  }
+}

+ 51 - 0
src/tenant/music/component/music-grid/index.tsx

@@ -0,0 +1,51 @@
+import { Grid, GridItem, Icon, Image, Loading } from 'vant'
+import { defineComponent, PropType } from 'vue'
+import styles from './index.module.less'
+import IconXin from '../images/icon-xin.png'
+
+export default defineComponent({
+  name: 'TheMusicGrid',
+  props: {
+    isHiddenTag: {
+      type: Boolean,
+      default: false
+    },
+    list: {
+      type: Array as any,
+      default: () => []
+    }
+  },
+  emits: ['goto'],
+  setup(props, { emit }) {
+    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}>
+                  {n.paymentType === 'CHARGE' && !props.isHiddenTag && (
+                    <span class={styles.albumType}>付费</span>
+                  )}
+                  <Image
+                    class={styles.image}
+                    width="100%"
+                    height="100%"
+                    fit="cover"
+                    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>
+    )
+  }
+})

部分文件因为文件数量过多而无法显示