Browse Source

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

lex 1 year ago
parent
commit
635cde6834
100 changed files with 4268 additions and 161 deletions
  1. BIN
      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. BIN
      src/tenant/images/bg-image.png
  19. BIN
      src/tenant/images/icon-search.png
  20. BIN
      src/tenant/images/icon-share.png
  21. 32 0
      src/tenant/layout/auth.module.less
  22. 115 0
      src/tenant/layout/auth.tsx
  23. BIN
      src/tenant/layout/images/bottom_bg.png
  24. BIN
      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. BIN
      src/tenant/member-center/images/1.png
  29. BIN
      src/tenant/member-center/images/2.png
  30. BIN
      src/tenant/member-center/images/3.png
  31. BIN
      src/tenant/member-center/images/4.png
  32. BIN
      src/tenant/member-center/images/5.png
  33. BIN
      src/tenant/member-center/images/6.png
  34. BIN
      src/tenant/member-center/images/7.png
  35. BIN
      src/tenant/member-center/images/8.png
  36. BIN
      src/tenant/member-center/images/contnt-bg.png
  37. BIN
      src/tenant/member-center/images/discount_bg.png
  38. BIN
      src/tenant/member-center/images/function-title.png
  39. BIN
      src/tenant/member-center/images/icon-arrow-line.png
  40. BIN
      src/tenant/member-center/images/icon-arrow.png
  41. BIN
      src/tenant/member-center/images/icon-logo-default.png
  42. BIN
      src/tenant/member-center/images/icon-logo.png
  43. BIN
      src/tenant/member-center/images/icon-member-active.png
  44. BIN
      src/tenant/member-center/images/icon-member-s.png
  45. BIN
      src/tenant/member-center/images/icon-selected.png
  46. BIN
      src/tenant/member-center/images/icon_discount.png
  47. BIN
      src/tenant/member-center/images/icon_gift.png
  48. BIN
      src/tenant/member-center/images/icon_video.png
  49. BIN
      src/tenant/member-center/images/info-title.png
  50. BIN
      src/tenant/member-center/images/member-bg.png
  51. BIN
      src/tenant/member-center/images/member_bg.png
  52. BIN
      src/tenant/member-center/images/member_logo.png
  53. BIN
      src/tenant/member-center/images/price-bg.png
  54. BIN
      src/tenant/member-center/images/record_bg.png
  55. BIN
      src/tenant/member-center/images/tip_bg.png
  56. BIN
      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. BIN
      src/tenant/music/album-detail/charge_bg.png
  60. BIN
      src/tenant/music/album-detail/header-bg.png
  61. BIN
      src/tenant/music/album-detail/iStart.png
  62. BIN
      src/tenant/music/album-detail/icon-hart-active.png
  63. BIN
      src/tenant/music/album-detail/icon-hart.png
  64. BIN
      src/tenant/music/album-detail/icon-menu.png
  65. BIN
      src/tenant/music/album-detail/icon-pan.png
  66. BIN
      src/tenant/music/album-detail/icon-start-active.png
  67. BIN
      src/tenant/music/album-detail/icon-start.png
  68. BIN
      src/tenant/music/album-detail/icon_music_list.png
  69. BIN
      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. BIN
      src/tenant/music/album-detail/oStart.png
  73. BIN
      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. BIN
      src/tenant/music/component/images/collection.png
  87. BIN
      src/tenant/music/component/images/collection_active.png
  88. BIN
      src/tenant/music/component/images/icon-play.png
  89. BIN
      src/tenant/music/component/images/icon-xin.png
  90. BIN
      src/tenant/music/component/images/icon_ai.png
  91. BIN
      src/tenant/music/component/images/icon_album.png
  92. BIN
      src/tenant/music/component/images/icon_album_active.png
  93. BIN
      src/tenant/music/component/images/icon_author.png
  94. BIN
      src/tenant/music/component/images/icon_download.png
  95. BIN
      src/tenant/music/component/images/icon_exquisite.png
  96. BIN
      src/tenant/music/component/images/icon_music_active.png
  97. BIN
      src/tenant/music/component/images/icon_share.png
  98. BIN
      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

BIN
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>

BIN
src/tenant/images/bg-image.png


BIN
src/tenant/images/icon-search.png


BIN
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}
+      </>
+    )
+  }
+})

BIN
src/tenant/layout/images/bottom_bg.png


BIN
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')

BIN
src/tenant/member-center/images/1.png


BIN
src/tenant/member-center/images/2.png


BIN
src/tenant/member-center/images/3.png


BIN
src/tenant/member-center/images/4.png


BIN
src/tenant/member-center/images/5.png


BIN
src/tenant/member-center/images/6.png


BIN
src/tenant/member-center/images/7.png


BIN
src/tenant/member-center/images/8.png


BIN
src/tenant/member-center/images/contnt-bg.png


BIN
src/tenant/member-center/images/discount_bg.png


BIN
src/tenant/member-center/images/function-title.png


BIN
src/tenant/member-center/images/icon-arrow-line.png


BIN
src/tenant/member-center/images/icon-arrow.png


BIN
src/tenant/member-center/images/icon-logo-default.png


BIN
src/tenant/member-center/images/icon-logo.png


BIN
src/tenant/member-center/images/icon-member-active.png


BIN
src/tenant/member-center/images/icon-member-s.png


BIN
src/tenant/member-center/images/icon-selected.png


BIN
src/tenant/member-center/images/icon_discount.png


BIN
src/tenant/member-center/images/icon_gift.png


BIN
src/tenant/member-center/images/icon_video.png


BIN
src/tenant/member-center/images/info-title.png


BIN
src/tenant/member-center/images/member-bg.png


BIN
src/tenant/member-center/images/member_bg.png


BIN
src/tenant/member-center/images/member_logo.png


BIN
src/tenant/member-center/images/price-bg.png


BIN
src/tenant/member-center/images/record_bg.png


BIN
src/tenant/member-center/images/tip_bg.png


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

BIN
src/tenant/music/album-detail/charge_bg.png


BIN
src/tenant/music/album-detail/header-bg.png


BIN
src/tenant/music/album-detail/iStart.png


BIN
src/tenant/music/album-detail/icon-hart-active.png


BIN
src/tenant/music/album-detail/icon-hart.png


BIN
src/tenant/music/album-detail/icon-menu.png


BIN
src/tenant/music/album-detail/icon-pan.png


BIN
src/tenant/music/album-detail/icon-start-active.png


BIN
src/tenant/music/album-detail/icon-start.png


BIN
src/tenant/music/album-detail/icon_music_list.png


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

BIN
src/tenant/music/album-detail/oStart.png


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

BIN
src/tenant/music/component/images/collection.png


BIN
src/tenant/music/component/images/collection_active.png


BIN
src/tenant/music/component/images/icon-play.png


BIN
src/tenant/music/component/images/icon-xin.png


BIN
src/tenant/music/component/images/icon_ai.png


BIN
src/tenant/music/component/images/icon_album.png


BIN
src/tenant/music/component/images/icon_album_active.png


BIN
src/tenant/music/component/images/icon_author.png


BIN
src/tenant/music/component/images/icon_download.png


BIN
src/tenant/music/component/images/icon_exquisite.png


BIN
src/tenant/music/component/images/icon_music_active.png


BIN
src/tenant/music/component/images/icon_share.png


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

Some files were not shown because too many files changed in this diff