Explorar o código

feat: 作品详情页

TIANYONG hai 4 meses
pai
achega
9e23c6a956

+ 4 - 2
src/components/col-header/index.tsx

@@ -52,9 +52,10 @@ export default defineComponent({
     },
     hideHeader: {
       type: Boolean,
-      default: true
+      default: false
     }
   },
+  emits: ['leftClick'],
   watch: {
     backIconColor() {
       // 设置返回按钮颜色
@@ -122,6 +123,7 @@ export default defineComponent({
       !browser().isApp && callBack && callBack()
     },
     onClickLeft() {
+      this.$emit('leftClick', null)
       if (browser().isApp) {
         postMessage({ api: 'goBack' })
       } else {
@@ -134,7 +136,7 @@ export default defineComponent({
   },
   render() {
     // 只有app里面才显示头部
-    return browser().isApp || !this.hideHeader ? (
+    return !this.hideHeader ? (
       <div>
         {this.$slots.content ? (
           <div

+ 1 - 0
src/components/col-result/index.module.less

@@ -23,6 +23,7 @@
     .van-empty__description {
       margin-top: 8px;
       padding: 0 30px;
+      color: #fff;
     }
   }
 

+ 32 - 0
src/helpers/utils.ts

@@ -7,6 +7,33 @@ import qs from 'query-string'
 export const browser = () => {
   const u = navigator.userAgent
   //   app = navigator.appVersion;
+  const isAndroid = /(?:Android)/.test(u);
+  const isFireFox = /(?:Firefox)/.test(u);
+  function isIpadFun() {
+    const ua = window.navigator.userAgent;
+    let IsIPad = false;
+    if (/ipad/i.test(ua)) {
+      IsIPad = true;
+    }
+    // iPad from IOS13
+    const macApp = ua.match(/Macintosh/i) != null;
+    if (macApp) {
+      // need to distinguish between Macbook and iPad
+      const canvas = document.createElement('canvas');
+      if (canvas != null) {
+        const context: any =
+          canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+        if (context) {
+          const info = context.getExtension('WEBGL_debug_renderer_info');
+          if (info) {
+            const renderer = context.getParameter(info.UNMASKED_RENDERER_WEBGL);
+            if (renderer.indexOf('Apple') != -1) IsIPad = true;
+          }
+        }
+      }
+    }
+    return IsIPad;
+  }
 
   let instance: any
   if (u.indexOf('ORCHESTRASTUDENT') > -1) {
@@ -34,6 +61,11 @@ export const browser = () => {
     //   u.indexOf('COLEXIUAPPA') > -1 ||
     //   u.indexOf('ORCHESTRASTUDENT') > -1 ||
     //   u.indexOf('Adr') > -1,
+    isTablet:
+      /(?:iPad|PlayBook)/.test(u) ||
+      (isAndroid && !/(?:Mobile)/.test(u)) ||
+      (isFireFox && /(?:Tablet)/.test(u)) ||
+      isIpadFun(),    
     isTeacher: u.indexOf('COLEXIUTEACHER') > -1,
     isStudent: u.indexOf('COLEXIUSTUDENT') > -1,
     isOrchestraStudent: u.indexOf('ORCHESTRASTUDENT') > -1, // 判断是否是管乐团学生端

+ 143 - 0
src/views/creation/audioVisualDraw.ts

@@ -0,0 +1,143 @@
+    /**
+     * 音频可视化
+     * @param audioDom
+     * @param canvasDom
+     * @param fftSize  2的幂数,最小为32
+     * 注意   由于ios低版本必须在用户操作之后才能初始化 createMediaElementSource 所以必须在用户操作之后初始化
+     */
+    export default function audioVisualDraw(audioDom: HTMLAudioElement, canvasDom: HTMLCanvasElement, fftSize = 128) {
+      type propsType = { canvWidth: number; canvHeight: number; canvFillColor: string; lineColor: string; lineGap: number }
+      // canvas
+      const canvasCtx = canvasDom.getContext("2d")!
+      let { width, height } = canvasDom.getBoundingClientRect()
+      // 向上取整,当with为小数或者小于当前dom时候,切换app之后 会出现黑边
+      width = Math.ceil(width)
+      height = Math.ceil(height)
+      canvasDom.width = width
+      canvasDom.height = height
+      // audio
+      //const audioCtx = new AudioContext()
+      //const source = audioCtx.createMediaElementSource(audioDom)
+      //const analyser = audioCtx.createAnalyser()
+      //analyser.fftSize = fftSize
+      //source?.connect(analyser)
+      //analyser.connect(audioCtx.destination)
+      //const dataArray = new Uint8Array(fftSize / 2)
+      const draw = (data: Uint8Array, ctx: CanvasRenderingContext2D, { lineGap, canvWidth, canvHeight, canvFillColor, lineColor }: propsType) => {
+        if (!ctx) return
+        const w = canvWidth
+        const h = canvHeight
+        fillCanvasBackground(ctx, w, h, canvFillColor)
+          // 可视化
+        const dataLen = data.length
+        let step = (w / 2 - lineGap * dataLen) / dataLen
+        step < 1 && (step = 1)
+        const midX = w / 2
+        const midY = h / 2
+        let xLeft = midX
+        for (let i = 0; i < dataLen; i++) {
+          const value = data[i]
+          const percent = value / 255 // 最大值为255
+          const barHeight = percent * midY
+          canvasCtx.fillStyle = lineColor
+          // 中间加间隙
+          if (i === 0) {
+            xLeft -= lineGap / 2
+          }
+          canvasCtx.fillRect(xLeft - step, midY - barHeight, step, barHeight)
+          canvasCtx.fillRect(xLeft - step, midY, step, barHeight)
+          xLeft -= step + lineGap
+        }
+        let xRight = midX
+        for (let i = 0; i < dataLen; i++) {
+          const value = data[i]
+          const percent = value / 255 // 最大值为255
+          const barHeight = percent * midY
+          canvasCtx.fillStyle = lineColor
+          if (i === 0) {
+            xRight += lineGap / 2
+          }
+          canvasCtx.fillRect(xRight, midY - barHeight, step, barHeight)
+          canvasCtx.fillRect(xRight, midY, step, barHeight)
+          xRight += step + lineGap
+        }
+      }
+      const fillCanvasBackground = (ctx: CanvasRenderingContext2D, w: number, h: number, colors: string) => {
+        ctx.clearRect(0, 0, w, h)
+        ctx.fillStyle = colors
+        ctx.fillRect(0, 0, w, h)
+      }
+      const requestAnimationFrameFun = () => {
+        // requestAnimationFrame(() => {
+        //   //analyser?.getByteFrequencyData(dataArray)
+        //   draw(generateMixedData(48), canvasCtx, {
+        //     lineGap: 2,
+        //     canvWidth: width,
+        //     canvHeight: height,
+        //     canvFillColor: "transparent",
+        //     lineColor: "rgba(255, 255, 255, 0.7)"
+        //   })
+        //   if (!isPause) {
+        //     requestAnimationFrameFun()
+        //   }
+        // })
+        const _time = setInterval(() => {
+            if (isPause) {
+              clearInterval(_time)
+              return
+            }
+            //analyser?.getByteFrequencyData(dataArray)
+            draw(generateMixedData(48), canvasCtx, {
+              lineGap: 2,
+              canvWidth: width,
+              canvHeight: height,
+              canvFillColor: "transparent",
+              lineColor: "rgba(255, 255, 255, 0.7)"
+            })
+        }, 300);
+      }
+      let isPause = true
+      const playVisualDraw = () => {
+        //audioCtx.resume()  // 重新更新状态   加了暂停和恢复音频音质发生了变化  所以这里取消了
+        isPause = false
+        requestAnimationFrameFun()
+      }
+      const pauseVisualDraw = () => {
+        isPause = true
+        requestAnimationFrame(()=>{
+          canvasCtx.clearRect(0, 0, width, height);
+        })
+        //audioCtx?.suspend()  // 暂停   加了暂停和恢复音频音质发生了变化  所以这里取消了
+        // source?.disconnect()
+        // analyser?.disconnect()
+      }
+      return {
+        playVisualDraw,
+        pauseVisualDraw
+      }
+    }
+
+export function generateMixedData(size: number) {
+  const dataArray = new Uint8Array(size);
+  const baseNoiseAmplitude = 30;
+  const minFrequency = 0.01;
+  const maxFrequency = 0.2;
+  const minAmplitude = 50;
+  const maxAmplitude = 150;
+
+  let lastAmplitude = maxAmplitude;  // 初始振幅设置为最大值
+  let lastFrequency = minFrequency + Math.random() * (maxFrequency - minFrequency);
+
+  for (let i = 0; i < size; i++) {
+      const decayFactor = 1 - (i / size);  // 使振幅随时间递减
+      const amplitude = lastAmplitude * decayFactor + (Math.random() - 0.5) * 10;
+      const frequency = lastFrequency + (Math.random() - 0.5) * 0.01;
+      const wave = amplitude * (0.5 + 0.5 * Math.sin(frequency * i));
+      const noise = Math.floor(Math.random() * baseNoiseAmplitude) - baseNoiseAmplitude / 2;
+      dataArray[i] = Math.min(255, Math.max(0, Math.floor(wave + noise)));
+      lastAmplitude += (amplitude - lastAmplitude) * 0.05;
+      lastFrequency += (frequency - lastFrequency) * 0.05;
+  }
+
+  return dataArray;
+}

BIN=BIN
src/views/creation/images/Landscape.png


BIN=BIN
src/views/creation/images/audioBg.png


BIN=BIN
src/views/creation/images/back.png


BIN=BIN
src/views/creation/images/back1.png


BIN=BIN
src/views/creation/images/bg.png


BIN=BIN
src/views/creation/images/edit.png


BIN=BIN
src/views/creation/images/icon-delete.png


BIN=BIN
src/views/creation/images/icon-download.png


BIN=BIN
src/views/creation/images/icon-share.png


BIN=BIN
src/views/creation/images/icon-zan.png


BIN=BIN
src/views/creation/images/midPlay.png


BIN=BIN
src/views/creation/images/music_bg.png


BIN=BIN
src/views/creation/images/upward.png


BIN=BIN
src/views/creation/images/videoBg.png


+ 778 - 474
src/views/creation/index.module.less

@@ -1,611 +1,915 @@
-.playSection {
-  min-height: 175px;
-
-  :global {
-    .vjs-poster {
-      background-size: cover;
-    }
-
-
-    .video-js .vjs-progress-control:hover .vjs-progress-holder {
-      font-size: inherit !important;
-      outline: none;
+* {
+  box-sizing: border-box;
+}
+.creationBg{
+  position: fixed;
+  z-index: -1;
+  width: 100%;
+  height: 100%;
+  min-height: 100vh;
+  top: 0;
+  left: 0;
+  background: url("./images/bg.png") no-repeat;
+  background-size: 100% 100%;
+}
+.creation{
+  :global{
+    .van-nav-bar .van-icon{
+      color: #ffffff;
     }
-
-    .video-js .vjs-slider:focus {
-      box-shadow: none !important;
-      text-shadow: none !important;
-      outline: none;
+  }
+  &.isScreenScroll,&.isShareScreenScroll{
+    :global{
+      .van-nav-bar .van-icon{
+        color: #333333;
+      }
     }
   }
 }
-
-@keyframes rotateImg {
-  100% {
-    transform: rotate(360deg);
-  }
+.singer{
+  text-align: center;
+  font-weight: 400;
+  font-size: 14px;
+  color: rgba(255,255,255,0.7);
+  line-height: 20px;
+  margin-bottom: 14vh;
 }
 
-.audioSection {
+.playSection{
+  height: 210px;
   position: relative;
-  background: url('./images/audio-banner-bg.png') no-repeat top center;
-  background-size: cover;
-  height: 175px;
-
-  .audioContainer {
+  &::after{
     position: absolute;
-    top: 0;
-    left: 50%;
-    width: 196px;
-    height: 35px;
-    transform: translate(-50%, 60px);
-
-    .waveActive,
-    .waveDefault {
+    content: "";
+    width: 100%;
+    height: 40px;
+    bottom: -40px;
+    left: 0;
+    background: linear-gradient( 180deg, #444B68 0%, rgba(57,77,95,0) 100%);
+    pointer-events: none;
+  }
+  :global {
+      .plyr {
+          width: 100%;
+          height: 100%;
+          z-index: initial;
+          .plyr__controls{
+              background: initial;
+              padding: 0 12px 2px;
+              opacity: 1 !important;
+              transform: translateY(0) !important;
+              pointer-events: initial !important;
+              .plyr__controls__item.plyr__progress__container{
+                  input[type=range]{
+                      color: #01c1b5;
+                      height: 10px;
+                  }
+                  input[type="range"]::-webkit-slider-runnable-track {
+                      height: 2px;
+                  }
+                  input[type="range"]::-webkit-slider-thumb {
+                      width: 6px;
+                      height: 6px;
+                      margin-top: -2px;
+                      box-shadow: initial;
+                  }
+                  .plyr__progress__buffer{
+                      height: 2px;
+                      color: rgba(1, 193, 181, 0.8);
+                      background-color: #fff;
+                      margin-top: -1px;
+                  }
+              }
+              .plyr__controls__item.plyr__time{
+                display: none;
+              }
+              .plyr__controls__item.plyr__control{
+                display: none;
+              }
+          }
+      }
+  }
+  .videoBox{
+    width: 100%;
+    height: 100%;
+  }
+  .audioBox{
       width: 100%;
       height: 100%;
-    }
+      background: url("./images/audioBg.png") no-repeat;
+      background-size: 100% 100%;
+      position: relative;
+      overflow: hidden;
+      .audioBga {
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -60%);
+        width: 100%;
+        height: 82%;
+      }
 
-    .waveDefault {
-      position: absolute;
-      top: 0;
-      left: 0;
-      background: url('./images/wave-1.png')no-repeat center left;
-      background-size: cover;
-    }
+      .audioBga1 {
+          position: absolute;
+          left: 0;
+          top: 16px;
+          width: 94px;
+      }
 
-    .waveActive {
-      position: absolute;
-      top: 0;
-      left: 0;
-      z-index: 1;
-      background: url('./images/wave-2.png')no-repeat center left;
-      background-size: cover;
-    }
+      .audioBga2 {
+          width: 192px;
+          position: absolute;
+          right: -24px;
+          top: 0;
+      }
+      :global {
+          .plyr {
+              position: absolute;
+              height: initial;
+              left: 0;
+              bottom: 0;
+              z-index: 2;
+          }
+      }
+      .audioVisualizer{
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%,-50%);
+          width: 280px;
+          height: 55px;
+      }
   }
-
-
-  .audioBox {
+  .playLarge{
     position: absolute;
     left: 50%;
-    transform: translate(-50%, 50%);
-    z-index: 2;
-    width: 74px;
-    height: 75px;
-    background: url('./images/audio-bg.png') no-repeat center;
-    background-size: contain;
-
-    // &::after {
-    //   content: '';
-    //   width: 134px;
-    //   height: 73px;
-    //   position: absolute;
-    //   left: 50%;
-    //   transform: translate(-50%, 50%);
-    //   background: url('./images/audio-shadow.png') no-repeat center;
-    //   background-size: contain;
-    //   z-index: -1;
-    // }
-    .audioPan {
-      position: absolute;
-      left: 8px;
-      top: 6px;
-      z-index: 8;
-      width: 59px;
-      height: 60px;
-      background: url('./images/audio-pan.png') no-repeat center;
-      background-size: contain;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-
-      animation: rotateImg 6s linear infinite;
-
-      &.imgRotate {
-        animation-play-state: paused;
-      }
-    }
-
-    .audioImg {
-      width: 32px;
-      height: 32px;
-      border-radius: 50%;
-      overflow: hidden;
-    }
-
-    .audioPoint {
-      position: absolute;
-      z-index: 9;
-      left: 50%;
-      top: 50%;
-      transform: translate(-50%, -50%);
-      width: 8px;
-      height: 8px;
-      background: url('./images/audio-point.png') no-repeat center;
-      background-size: contain;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    width: 48px;
+    height: 48px;
+    background: url("./images/midPlay.png") no-repeat;
+    background-size: 100% 100%;
+    z-index: 12;
+    display: none;
+    &.playIngShow{
+      display: initial;
     }
-
-    .audioZhen {
-      position: absolute;
-      z-index: 9;
-      right: -4px;
-      top: -33px;
-      width: 26px;
-      height: 87px;
-      background: url('./images/audio-zhen.png') no-repeat center;
-      background-size: contain;
-      transition: transform .5s ease-in-out;
-
-      &.active {
-        transform: rotate(92deg) translate3d(0, 0, 3px);
-        transition: transform .5s ease-in-out;
-      }
-    }
-  }
-
-}
-
-.controls {
-  position: absolute;
-  left: 0;
-  bottom: 0;
-  right: 0;
-  height: 44px;
-  display: flex;
-  flex-direction: column;
-  justify-content: space-between;
-  flex-direction: row;
-  transition: all 0.5s;
-  padding: 0 12px;
-
-  &>div {
-    display: flex;
-    align-items: center;
   }
-
-  &.hide {
-    transform: translateY(100%);
+  .mediaTimeCon{
+    display: none;
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, .3);
   }
-
-
-
-  .actionBtn {
-    line-height: 0;
-    margin-right: 4px;
-
-    img {
-      width: 14px;
-      height: 14px;
-      margin-bottom: -2px;
+  &.mediaTimeShow{
+    .mediaTimeCon{
+      display: block;
     }
   }
-
-  .time {
+  .mediaTime{
+    position: absolute;
+    left: 50%;
+    transform: translateX(-50%);
+    bottom: 95px;
     display: flex;
-    justify-content: space-between;
-    flex: 1;
-    min-width: 86px;
-    font-size: 12px;
-    color: #131415;
+    font-weight: 500;
+    font-size: 16px;
+    color: #B8E8FF;
     line-height: 20px;
-
-    span {
-      font-size: 12px;
-      padding: 0 1px;
+    z-index: 10;
+    text-shadow: 0px 1px 1px rgba(0,0,0,0.5);
+    & div:first-child{
+      width: 50px;
+      text-align: right;
+    }
+    .note{
+      margin: 0 4px;
+    }
+    .duration{
+      color: #fff;
     }
   }
-
-  .slider {
+  .landscapeScreen{
+    width: 32px;
+    height: 32px;
+    position: absolute;
+    background: url("./images/Landscape.png") no-repeat;
+    background-size: 26px 26px;
+    background-position: center center;
+    right: 7px;
+    top: 7px;
+    z-index: 15;
+  }
+  .staffBoxCon{
+    position: absolute;
     width: 100%;
-    margin: 0 12px;
-    --van-slider-bar-height: 4px;
-    --van-slider-button-width: 13px !important;
-    --van-slider-button-height: 13px !important;
-    --van-slider-inactive-background: #fff;
-    --van-slider-inactive-background-color: #fff;
-    --van-slider-active-background: #2DC7AA !important;
-
-    :global {
-
-      .van-loading {
-        width: 100%;
-        height: 100%;
-      }
-
+    height: 100%;
+    left: 0;
+    top: 0;
+    z-index: 1;
+    overflow: hidden;
+    visibility: hidden;
+    &.staffBoxShow{
+      visibility: initial;
     }
   }
-
-}
-
-.userSection {
-  padding: 15px 12px !important;
-  background-color: transparent !important;
-
-  .userLogoSection {
-    width: 44px;
-    height: 44px;
-    border: 1px solid #FFFFFF;
-    margin-right: 10px;
-    border-radius: 50%;
-    flex-shrink: 0;
-    position: relative;
-
-    .showMemeber {
-      position: absolute;
-      bottom: 1px;
-      right: -3px;
-      width: 18px;
-      height: 18px;
-    }
-
-    &.userVip {
-      border: 1px solid #F0AF88;
-
-      .showMemeber {
-        background: url('./images/icon-vip.png') no-repeat center;
-        background-size: contain;
-      }
+  .staffBox{
+    width: 100%;
+    height: calc(var(--staffBoxHeight) + 12px);
+    position: absolute;
+    bottom: 0;
+    padding-bottom: 12px;
+    background: linear-gradient( 180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.7) 100%);
+    .staff{
+      width: 100%;
+      height: 100%;
+      padding-left: 10px;
     }
-
-    &.userSVip {
-
-      border: 1px solid #F0AF88;
-
-      .showMemeber {
-        background: url('./images/icon-svip.png') no-repeat center;
-        background-size: contain;
-      }
-
+    .mask{
+      position: absolute;
+      z-index: 6;
+      width: 100%;
+      height: 100%;
     }
   }
+}
 
-  .userLogo {
-    width: 44px;
-    height: 44px;
-    border-radius: 50%;
-    overflow: hidden;
-  }
+.musicSection {
+  width: 100%;
+  min-height: calc(var(--creationHeight, 100vh) - var(--barheight) - 20px - 14vh - 210px - 55px - 80px);
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+  padding: 10px 12px 0;
 
-  .userInfo {
-    .name {
+  .avatarInfoBox{
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    .avatar{
       display: flex;
       align-items: center;
-      font-size: 16px;
-      font-weight: 500;
-      color: #333333;
-      line-height: 22px;
-
-      span {
-        display: inline-block;
-        white-space: nowrap;
+      .userLogo{
+        width: 44px;
+        height: 44px;
+        border: 1px solid #FFFFFF;
+        margin-right: 10px;
+        border-radius: 50%;
         overflow: hidden;
-        text-overflow: ellipsis;
-        max-width: 100px;
+      }
+      .infoCon{
+        .info{
+          display: flex;
+          align-items: center;
+          .userName{
+            font-weight: 500;
+            font-size: 16px;
+            color: #FFFFFF;
+            line-height: 22px;
+            overflow: hidden;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+            max-width: 160px;
+          }
+          .iconMember{
+            margin-left: 6px;
+            width: 14px;
+            height: 14px;
+          }
+        }
+        .sub{
+          margin-top: 2px;
+          font-weight: 400;
+          font-size: 12px;
+          color: #FFFFFF;
+          line-height: 17px;
+        }
       }
     }
-
-    .sub {
-      padding-top: 2px;
-      font-size: 12px;
-      color: #777777;
-      line-height: 17px;
-    }
-
-    .iconMember {
-      margin-left: 6px;
-      width: 14px;
-      height: 14px;
-    }
-  }
-
-  .zan {
-    background: #FFFFFF;
-    border-radius: 13px;
-    font-size: 14px;
-    color: #777777;
-    line-height: 20px;
-    padding: 4px 9px 3px;
-    display: inline-flex;
-    align-items: center;
-
-    &.zanActive {
-      background: #F7EEEE;
-      color: #FF6A6A;
-    }
-
-    .iconZan {
-      width: 18px;
-      height: 18px;
-      margin-right: 2px;
-    }
-  }
-}
-
-.musicSection {
-  margin: 0 13px 12px;
-  padding: 14px 12px;
-  background: #FFFFFF;
-  border-radius: 10px;
-
-  .musicName {
-    font-size: 15px;
-    font-weight: 500;
-    color: #333333;
-    line-height: 21px;
-    // display: flex;
-    // align-items: center;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-    max-width: 230px;
-
-    .musicTag {
-      margin-right: 6px;
-      padding: 1px 6px;
-      font-size: 12px;
-      color: #FF7B31;
-      line-height: 17px;
-      background: rgba(255, 166, 115, 0.07);
-      border-radius: 9px;
-      border: 1px solid #FFBF9A;
+    .linkes{
+      display: flex;
+      align-items: center;
+      border-radius: 13px;
+      padding: 4px 8px 3px;
+      background-color: rgba(255,255,255,.12);
       font-weight: 400;
-      vertical-align: text-bottom;
-      display: inline-block;
+      font-size: 14px;
+      color: #FFFFFF;
+      .iconZan{
+        width: 18px;
+        height: 18px;
+        margin-right: 2px;
+      }
     }
   }
-
-  .musicDesc {
-    padding-top: 8px;
+  .textEllipsis{
+    margin-top: 10px;
+    font-weight: 400;
     font-size: 14px;
-    color: #777777;
+    color: #FFFFFF;
     line-height: 20px;
+    :global{
+      .van-text-ellipsis__action{
+        color: #5CCEFF;
+        font-weight: 500;
+        margin-left: 2px;
+        &:active{
+          opacity: 1;
+        }
+      }
+    }
   }
 }
 
 .likeSection {
-  margin: 0 13px 12px;
-  background: #FFFFFF;
+  margin: 20px 12px;
+  background: rgba(255,255,255,.09);
   border-radius: 10px;
-  padding: 10px 12px;
-
+  padding: 12px 12px 0 12px;
+  overflow: hidden;
   .likeTitle {
     display: flex;
     align-items: center;
     font-size: 17px;
     font-weight: 600;
-    color: #333333;
+    color: #ffffff;
     line-height: 24px;
-    padding-bottom: 8px;
-
     &::before {
       display: inline-block;
       content: '';
       width: 4px;
       height: 14px;
       border-radius: 2px;
-      background: linear-gradient(to bottom, #59E5D5, #2DC7AA);
+      background: #2dc7aa;
       margin-right: 6px;
     }
   }
-}
-
-.likeItem {
-  padding: 16px 0;
+  .likeItem {
+    padding: 13px 0 16px;
+    background-color: initial;
+    border-bottom: 1px solid rgba(242,242,242,0.12);
+    &.likeItemLast{
+      border-bottom: none;
+    }
+    .userLogo {
+      border-radius: 50%;
+      overflow: hidden;
+      width: 42px;
+      height: 42px;
+      margin-right: 7px;
+    }
 
-  .userLogo {
-    border-radius: 50%;
-    overflow: hidden;
-    width: 42px;
-    height: 42px;
-    margin-right: 7px;
-  }
+    .userInfo {
+      .name {
+        font-size: 16px;
+        font-weight: 500;
+        color: #ffffff;
+        line-height: 22px;
+      }
 
-  .userInfo {
-    .name {
-      font-size: 16px;
-      font-weight: 500;
-      color: #333333;
-      line-height: 22px;
+      .sub {
+        padding-top: 2px;
+        font-size: 13px;
+        color: #ffffff;
+        line-height: 18px;
+      }
     }
 
-    .sub {
-      padding-top: 2px;
+    .time {
+      font-weight: 400;
       font-size: 13px;
-      color: #777777;
+      color: #FFFFFF;
       line-height: 18px;
     }
   }
-
-  .time {
-    font-size: 13px;
-    color: #777777;
-    line-height: 18px;
+  .mEmpty{
+    padding: 0;
+  }
+  .btnImg{
+    display: flex;
+    justify-content: center;
+    margin-top: 3px;
+    margin-bottom: 12px;
+    &>img{
+      width: 88px;
+      height: 31px;
+      &:active{
+        opacity: 0.8;
+      }
+    }
   }
 }
 
-
+.upward{
+  padding-top: 12px;
+  display: flex;
+  justify-content: center;
+  height: 55px;
+  background: linear-gradient(180deg, rgba(42, 78, 85, 0) 0%, rgba(43, 78, 85, 0.7) 19%, #2b4e55 100%);
+  > img{
+    width: 19px;
+    height: 15px;
+  }
+}
 .bottomSection {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  background-color: #fff;
-  padding: 15px 12px;
-
+  padding: 0 12px 0 20px;
+  width: 100%;
+  height: 80px;
+  background: #1F1F1F;
+  box-shadow: 0px -1px 10px 0px rgba(0,0,0,0.05);
   .bottomShare {
     display: flex;
     align-items: center;
-
     p {
-      padding: 0 15px;
-      text-align: center;
-      line-height: 0;
-
-      &:first-child {
-        padding-left: 5px;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      margin-right: 28px;
+      &:last-child{
+        margin-right: 0;
       }
     }
-
     img {
       width: 18px;
       height: 18px;
     }
-
     span {
-      padding-top: 8px;
+      margin-top: 8px;
+      font-weight: 400;
       font-size: 12px;
-      color: #333333;
+      color: #ffffff;
       line-height: 17px;
-      display: block;
     }
   }
 
   .btnEdit {
-    font-size: 14px;
-    font-weight: 500;
-    background: #2DC7AA;
-    color: #FFFFFF;
-    line-height: 22px;
-    min-width: 80px;
+    width: 80px;
     height: 30px;
-    border: none;
   }
 }
 
 .popupContainer {
-  width: 80%;
+  width: 287px;
+  background: rgba(255,255,255,0.31);
+  border-radius: 12px !important;
+  border: 1px solid rgba(255,252,252,0.53);
+  padding: 9px 8px;
+  overflow: initial;
+  .prompt{
+    width: 151px;
+    height: 32px;
+    position: absolute;
+    top: -7px;
+    left: 50%;
+    transform: translateX(-50%);
+  }
+  .deleteBox{
+    background: linear-gradient( 224deg, #ECF5FF 0%, #D5E8FF 100%);
+    border-radius: 12px;
+    padding: 37px 0 15px;
+  }
 
 
   .popupContent {
-    padding: 29px 0 25px;
     text-align: center;
-    font-size: 18px;
-    font-weight: 500;
-    color: #333333;
-    line-height: 25px;
+    font-weight: 400;
+    font-size: 16px;
+    color: #777777;
+    line-height: 26px;
   }
 
   .popupBtnGroup {
-    text-align: center;
-    margin-bottom: 22px;
-
-    :global {
-      .van-button {
-        height: 40px;
-        font-size: 16px;
-        font-weight: 400 !important;
-        line-height: 22px;
-        min-width: 122px;
-
-        &:last-child {
-          margin-left: 10px;
-          background: #2DC7AA;
-          border: none;
-        }
+    margin-top: 18px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    >img{
+      width: 118px;
+      height: 37px;
+      & + img{
+        margin-left: 11px;
       }
     }
   }
 }
-
-.cellGroup {
+// 分享样式
+.playSection.notLoaded{
+  :global{
+    .plyr .plyr__controls {
+      display: none;
+    }
+  }
+}
+.logoDownload{
   display: flex;
-  flex-wrap: wrap;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 13px;
+  position: relative;
+  &::after{
+    content: "";
+    position: absolute;
+    bottom: 0;
+    left: 13px;
+    width: calc(100% - 26px);
+    height: 1px;
+    background-color: rgba(255, 255, 255, 0.3) ;
+  }
+  .logoImg{
+    width: 159px;
+    height: 29px;
+  }
+  .logTit{
+    font-weight: 400;
+    font-size: 14px;
+    color: #FFFFFF;
+    line-height: 20px;
+    padding: 2px 10px;
+    border-radius: 20px;
+    border: 1px solid rgba(255, 255, 255, 0.5);
+  }
 }
-
-.cell {
-  // display: flex;
-  // flex-direction: column;
-  width: 96px;
-  margin-right: 18px;
-  margin-bottom: 18px;
-
-  &:nth-child(3n + 3) {
-    margin-right: 0;
+.isShareScreenScroll{
+  .logoDownload{
+    background-color: #ffffff;
+    .logTit{
+      background: #2dc7aa;
+      border: none;
+      padding: 3px 11px;
+    }
   }
-
-  .cellImg {
+}
+.singerBox{
+  height: 20vh;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+  .musicSheetName{
+    width: 200px;
+    margin: 0 auto 10px;
+    :global{
+      .van-notice-bar{
+          padding: 0;
+          height: 28px;
+          font-weight: 600;
+          font-size: 20px;
+          color: #FFFFFF;
+          line-height: 28px;
+          .van-notice-bar__content{
+            min-width: 100%;
+            text-align: center;
+          }
+      }
+    }
+  }
+  .singerName{
+    text-align: center;
+    font-weight: 400;
+    font-size: 14px;
+    color: rgba(255,255,255,0.7);
+    line-height: 20px;
+    margin-bottom: 10px;
+  }
+}
+.musicShareSection{
+  min-height: calc(var(--creationHeight, 100vh) - var(--barheight) - 20vh - 210px - 55px);
+}
+.likeShareItem{
+  background-color: initial;
+  padding: 0;
+  margin-top: 25px;
+  &:first-child{
+    margin-top: 20px;
+  }
+  &.likeShareItemLast{
+    padding-bottom: 20px;
+  }
+  .audioImgBox{
     position: relative;
-
-    &::before {
-      content: '';
+    width: 51px;
+    height: 51px;
+    margin-right: 14px;
+    .audioPan{
       position: absolute;
+      width: 100%;
+      height: 100%;
       right: -6px;
-      top: 3px;
-      z-index: 8;
-      width: 84px;
-      height: 84px;
-      background: url('./images/audio-pan.png') no-repeat center;
-      background-size: contain;
-      display: flex;
-      align-items: center;
-      justify-content: center;
+      top: 0;
     }
-
-    .iconZan {
+    .muploader{
+      position: relative;
+      z-index: 1;
+      width: 100%;
+      height: 100%;
+      border-radius: 8.5px;
+    }
+    .imgLabel{
       position: absolute;
-      bottom: 4px;
-      left: 4px;
+      right: 0;
+      top: 0;
+      width: 28px;
+      height: 14px;
       z-index: 10;
-      padding: 3px;
-      background: rgba(67, 67, 67, 0.3);
-      border-radius: 8px;
-      backdrop-filter: blur(4px);
-
-      font-size: 9px;
-      font-weight: 500;
+    }
+  }
+  .userInfo{
+    .musicSheetName{
+      font-weight: 600;
+      font-size: 16px;
       color: #FFFFFF;
-      line-height: 13px;
+      line-height: 22px;
+      width: 200px;
+    }
+    .usernameCon{
       display: flex;
       align-items: center;
-
-      &::before {
-        content: '';
-        display: inline-block;
-        width: 12px;
-        height: 12px;
-        background: url('./images/icon-z.png') no-repeat center;
-        background-size: contain;
+      margin-top: 6px;
+      .likeNum{
+        display: flex;
+        align-items: center;
+        border-radius: 3px;
+        background-color: rgba(255,255,255,.22);
+        padding: 1px 2px;
+        img{
+          width: 14px;
+          height: 15px;
+        }
+        span{
+          font-weight: 400;
+          font-size: 10px;
+          color: #FFFFFF;
+          line-height: 1;
+          margin-left: 2px;
+        }
+      }
+      .username{
+        max-width: 160px;
+        margin-left: 4px;
+        font-weight: 400;
+        font-size: 13px;
+        color: #DEDEDE;
+        line-height: 13px;
       }
     }
   }
+  :global{
+    .van-cell__value{
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+    }
+  }
+  .playImg{
+    width: 20px;
+    height: 20px;
+  }
+}
+.isEmpty{
+  height: calc(var(--creationHeight, 100vh) - var(--barheight));
+  display: flex;
+  align-items: center;
+}
 
-  .cellImage {
-    position: relative;
-    width: 88px;
-    height: 88px;
-    border-radius: 12px;
-    z-index: 9;
-
-    :global {
-      img {
-        border-radius: 12px;
+// 平板样式
+.creation{
+  &.creationTablet{
+    .playSection{
+      height: 340px;
+      .audioBox {
+        .audioBga{
+          width: 80%;
+          height: 62%;
+        }
+      }
+    }
+    .musicSection{
+      min-height: calc(var(--creationHeight, 100vh) - var(--barheight) - 20px - 14vh - 340px - 55px - 80px);
+    }
+    .musicShareSection{
+      min-height: calc(var(--creationHeight, 100vh) - var(--barheight) - 20vh - 340px - 55px);
+    }
+    .wxpopup{
+      img{
+        width: 54%;
+        margin-right: 18px;
       }
     }
   }
+}
 
-  .cellTitle {
-    font-size: 13px;
-    color: #131415;
-    line-height: 18px;
-    margin: 8px 0 6px;
+@keyframes rotate {
+  from {
+      transform: rotate(0deg);
   }
+  to {
+      transform: rotate(360deg);
+  }
+}
+.loadingPop {
+  position: fixed;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  width: 100%;
+  height: 100%;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  z-index: 9999999;
+  background: rgba(0, 0, 0, .5);
+  .loadingCssBox{
+      width: 27px;
+      height: 27px;
+      display: flex;
+      justify-content: space-between;
+      flex-wrap: wrap;
+      align-content: space-between;
+      margin-bottom: 24px;
+      animation: rotate 1.5s linear infinite;
+      .loadingCssItem{
+          width: 11px;
+          height: 11px;
+          border-radius: 50%;
+          background: #11ffd4;
+          opacity: 0.5;
+          &:nth-child(2){
+              opacity:1;
+          }
+      }
+  }
+  .loadingTip {
+      font-size: 14px;
+      color: #fff;
+  }
+}
 
-  .users {
-    display: flex;
-    align-items: center;
+.wxpopup {
+  width: 100%;
+  height: 100%;
+  min-height: 100vh;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 9999;
+  text-align: right;
 
-    .userImg {
-      width: 20px;
-      height: 20px;
-      border-radius: 50%;
-      overflow: hidden;
-      margin-right: 4px;
-      flex-shrink: 0;
-    }
+  img {
+    width: 88%;
+    margin-right: 6%;
+  }
+}
 
-    .name {
-      font-size: 12px;
-      color: #402424;
-      line-height: 14px;
+// :global{
+//   .dialogMusicClass.van-dialog{
+//     width: 300px;
+//     .van-dialog__message{
+//       font-weight: 500;
+//       line-height: 24px;
+//     }
+//   }
+// }
+
+// 横屏样式
+.creation.creationTablet{
+  .playSection.isLandscapeScreen{
+    .audioBox {
+      .audioBga{
+        width: 72%;
+        height: 56%;
+      }
+    }
+  }
+}
+.playSection.isLandscapeScreen{
+  overflow: hidden;
+  position: fixed;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  width: 100%;
+  height: 100%;
+  min-height: 100vh;
+  z-index: 1000;
+  .landscapeScreen{
+    display: none;
+  }
+  :global {
+    .plyr{
+      .plyr__controls{
+        padding: 0 20px 20px;
+        .plyr__controls__item.plyr__control{
+          display: block;
+          padding: 0;
+          width: 18px;
+          height: 18px;
+          &:hover{
+              background: initial;
+          }
+          .icon--pressed{
+              width: 100%;
+              height: 100%;
+              background: url("./images/pause1.png") no-repeat;
+              background-size: 100% 100%;
+              use{
+                  display: none;
+              }
+          }
+          .icon--not-pressed{
+              width: 100%;
+              height: 100%;
+              background: url("./images/play1.png") no-repeat;
+              background-size: 100% 100%;
+              use{
+                  display: none;
+              }
+          }
+        }
+        .plyr__controls__item.plyr__progress__container{
+          margin-left: 9px;
+          input[type=range]{
+              color: #01c1b5;
+              height: 20px;
+          }
+          input[type="range"]::-webkit-slider-runnable-track {
+              height: 4px;
+          }
+          input[type="range"]::-webkit-slider-thumb {
+              width: 12px;
+              height: 12px;
+              margin-top: -4px;
+              box-shadow: initial;
+          }
+          .plyr__progress__buffer{
+              height: 4px;
+              color: rgba(1, 193, 181, 0.8);
+              background-color: #fff;
+              margin-top: -2px;
+          }
+        }
+        .plyr__controls__item.plyr__time{
+            font-weight: 500;
+            font-size: 14px;
+            color: #FFFFFF;
+            display: initial;
+            &.plyr__time--current{
+                margin-left: 9px;
+            }
+        }
+      }
+    }
+  }
+  .backBox{
+    position: absolute;
+    left: 15px;
+    top: 15px;
+    display: flex;
+    z-index: 10;
+    .backImg{
+      width: 26px;
+      height: 26px;
+    }
+    .musicDetail{
+      margin-left: 10px;
+      .musicSheetName{
+        width: 300px;
+        margin-top: 4px;
+        :global{
+          .van-notice-bar{
+              padding: 0;
+              height: 20px;
+              font-weight: 600;
+              font-size: 18px;
+              color: #FFFFFF;
+              line-height: 20px;
+          }
+      }
+      }
+      .username{
+        margin-top: 2px;
+        font-weight: 400;
+        font-size: 12px;
+        color: rgba(255, 255, 255, 0.7);
+        line-height: 18px
+      }
+    }
+  }
+  .staffBox{
+    height: calc(var(--staffBoxHeight) + 44px);
+    padding-bottom: 44px;
+    .staff{
+      padding-left: 20px;
     }
   }
 }

+ 559 - 261
src/views/creation/index.tsx

@@ -1,13 +1,12 @@
-import { defineComponent, onMounted, onUnmounted, reactive, watch } from 'vue'
+import { defineComponent, onMounted, onUnmounted, reactive, watch, ref } from 'vue'
 // import WaveSurfer from 'wavesurfer.js';
 import styles from './index.module.less'
 import MSticky from '@/components/col-sticky'
 import MHeader from '@/components/col-header'
-import { Button, Cell, Dialog, Image, List, Popup, Slider, Toast } from 'vant'
+import { Button, Cell, Dialog, Image, List, Popup, Slider, Toast, Sticky, NoticeBar } from 'vant'
 import iconDownload from './images/icon-download.png'
 import iconShare from './images/icon-share.png'
 import iconDelete from './images/icon-delete.png'
-// import iconMember from './images/icon-member.png'
 import iconVip from './images/icon-vip.png'
 import iconSVip from './images/icon-svip.png'
 import iconZan from './images/icon-zan.png'
@@ -28,28 +27,34 @@ import dayjs from 'dayjs'
 import { nextTick } from 'process'
 import MVideo from '@/components/col-video-tcplayer'
 import ShareModel from './share-model'
-import { usePageVisibility } from '@vant/use'
+import { usePageVisibility, useEventListener } from '@vant/use'
+import "plyr/dist/plyr.css";
+import Plyr from "plyr";
+import { generateMixedData } from "./audioVisualDraw"
+import backImg from "./images/back.png";
+import back1Img from "./images/back1.png";
+import musicBg from "./images/music_bg.png";
+import videobg from "./images/videobg.png";
+import iconUpward from './images/upward.png';
+import iconEdit from './images/edit.png';
+import iconMember from './images/icon-member.png';
+import TextEllipsis from './text-ellipsis/index';
 
 export default defineComponent({
   name: 'creation-detail',
   setup() {
+    const {isApp, isTablet} = browser()
     const route = useRoute()
     const router = useRouter()
-    const audioId = 'a' + +Date.now() + Math.floor(Math.random() * 100)
-
+    const isScreenScroll = ref(false)
+    const mStickyBottom = ref()
+    const mStickyUpward = ref()
     const state = reactive({
       id: route.query.id,
       deleteStatus: false,
       shareStatus: false,
       playType: '' as 'Audio' | 'Video' | '', // 播放类型
       musicDetail: {} as any,
-      timer: null as any,
-      audioWidth: 0,
-      paused: true,
-      currentTime: 0,
-      duration: 0.1,
-      loop: false,
-      dragStatus: false, // 是否开始拖动
       isClick: false,
       list: [] as any,
       listState: {
@@ -60,35 +65,30 @@ export default defineComponent({
       params: {
         page: 1,
         rows: 20
-      }
-    })
-    const audioDom = new Audio()
-    audioDom.controls = true
-    audioDom.style.width = '100%'
-    audioDom.className = styles.audio
-
-    /** 改变播放时间 */
-    const handleChangeTime = (val: number) => {
-      state.currentTime = val
-      clearTimeout(state.timer)
-      state.timer = setTimeout(() => {
-        // audioRef.value.currentTime = val;
-        audioDom.currentTime = val
-        state.timer = null
-      }, 60)
-    }
-
-    // 切换音频播放
-    const onToggleAudio = (e: any) => {
-      e.stopPropagation()
-      if (audioDom.paused) {
-        audioDom.play()
-      } else {
-        audioDom.pause()
-      }
+      },
+      _plrl: null as any,
+      heightV:0,
+      heightB:0
+    });
 
-      state.paused = audioDom.paused
-    }
+    const plyrState = reactive({
+      duration: 0,
+      currentTime: 0,
+      mediaTimeShow: false,
+      playIngShow: true
+    })
+    // 谱面
+    const staffState = reactive({
+      staffSrc: "",
+      isShow: false,
+      height:"initial",
+      speedRate:1,
+      musicRenderType:"staff",
+      partIndex: 0
+    })
+    const isLandscapeScreen = ref(false)
+    const staffDom= ref<HTMLIFrameElement>()
+    const {playStaff, pauseStaff, updateProgressStaff} = staffMoveInstance()
 
     // 获取列表
     const getStarList = async () => {
@@ -117,87 +117,29 @@ export default defineComponent({
       }
     }
 
-    const initAudio = () => {
-      audioDom.src = state.musicDetail.videoUrl
-      audioDom.load()
-      audioDom.oncanplaythrough = () => {
-        state.paused = audioDom.paused
-        state.duration = audioDom.duration
-      }
-      // 播放时监听
-      audioDom.addEventListener('timeupdate', () => {
-        state.duration = audioDom.duration
-        state.currentTime = audioDom.currentTime
-        const rate = (state.currentTime / state.duration) * 100
-        state.audioWidth = rate > 100 ? 100 : rate
-      })
-      audioDom.addEventListener('ended', () => {
-        state.paused = audioDom.paused
-      })
-      // const wavesurfer = WaveSurfer.create({
-      //   container: document.querySelector(`#${audioId}`) as HTMLElement,
-      //   waveColor: '#fff',
-      //   progressColor: '#2FA1FD',
-      //   url: state.musicDetail.videoUrl,
-      //   cursorWidth: 0,
-      //   height: 35,
-      //   width: 'auto',
-      //   normalize: true,
-      //   // Set a bar width
-      //   barWidth: 2,
-      //   // Optionally, specify the spacing between bars
-      //   barGap: 2,
-      //   // And the bar radius
-      //   barRadius: 4,
-      //   barHeight: 0.6,
-      //   autoScroll: true,
-      //   /** If autoScroll is enabled, keep the cursor in the center of the waveform during playback */
-      //   autoCenter: true,
-      //   hideScrollbar: false,
-      //   media: audioDom
-      // });
-
-      // wavesurfer.once('interaction', () => {
-      //   // wavesurfer.play();
-      // });
-      // wavesurfer.once('ready', () => {
-      //   state.paused = audioDom.paused;
-      //   state.duration = audioDom.duration;
-      // });
-
-      // wavesurfer.on('finish', () => {
-      //   state.paused = true;
-      // });
-
-      // // 播放时监听
-      // audioDom.addEventListener('timeupdate', () => {
-      //   state.currentTime = audioDom.currentTime;
-      // });
-    }
 
     // 删除作品
     const onDelete = async () => {
       try {
-        await api_userMusicRemove({ id: state.id })
-
+        await api_userMusicRemove({ id: state.id });
         setTimeout(() => {
-          state.deleteStatus = false
-          Toast('删除成功')
-        }, 100)
+          state.deleteStatus = false;
+          Toast('删除成功');
+        }, 100);
 
         setTimeout(() => {
-          if (browser().isApp) {
+          if (isApp) {
             postMessage({
               api: 'goBack'
-            })
+            });
           } else {
-            router.back()
+            router.back();
           }
-        }, 1200)
+        }, 1200);
       } catch {
         //
       }
-    }
+    };
 
     // 下载
     const onDownload = async () => {
@@ -206,19 +148,356 @@ export default defineComponent({
         content: {
           url: state.musicDetail.videoUrl
         }
+      });
+    };
+    // 滚动事件
+    const cleanScrollEvent: any = useEventListener('scroll', () => {
+      const height =
+        window.scrollY ||
+        document.documentElement.scrollTop
+        // 防止多次调用
+        if(height > 0 && isScreenScroll.value === false){
+          isScreenScroll.value = true
+          setStatusBarTextColor(false)
+        }
+        if(height <= 0){
+          isScreenScroll.value = false
+          setStatusBarTextColor(true)
+        }
+    })
+    // 设置导航栏颜色
+    function setStatusBarTextColor(isWhite:boolean){
+      postMessage({
+        api: 'setStatusBarTextColor',
+        content: { statusBarTextColor: isWhite }
       })
     }
-
-    const pageVisibility = usePageVisibility()
+    const pageVisibility = usePageVisibility();
     watch(pageVisibility, value => {
       if (value === 'hidden') {
-        if (audioDom) {
-          audioDom.pause()
-          state.paused = audioDom.paused
+        state._plrl?.pause();
+      }
+    });
+    // 初始化 媒体播放
+    function initMediaPlay(){
+      const id = state.playType === "Audio" ? "#audioMediaSrc" : "#videoMediaSrc";
+      state._plrl = new Plyr(id, {
+        controls: ["play", "progress", "current-time", "duration"],
+        fullscreen: {
+          enabled: false,
+          fallback: false
         }
+      });
+      const player = state._plrl
+        // 创建音波数据
+      if(state.playType === "Audio"){
+        const audioDom = document.querySelector("#audioMediaSrc") as HTMLAudioElement
+        const canvasDom = document.querySelector("#audioVisualizer") as HTMLCanvasElement
+        const { pauseVisualDraw, playVisualDraw } = audioVisualDraw(audioDom, canvasDom)
+        player.on('play', () => {
+          playVisualDraw()
+        });
+        player.on('pause', () => {
+          pauseVisualDraw()
+        });
       }
-    })
+      // player.on('loadedmetadata', () => {
+      //   player.currentTime = playProgressData.playProgress
+      // });
+      player.on("timeupdate", ()=>{
+        plyrState.currentTime = player.currentTime
+      })
+      player.on('play', () => {
+        plyrState.playIngShow = false
+        playStaff()
+      });
+      player.on('pause', () => {
+        plyrState.playIngShow = true
+        pauseStaff()
+      });
+      player.on('ended', () => {
+        player.currentTime = 0
+        if(!player.playing){
+          setTimeout(() => {
+            updateProgressStaff(player.currentTime)
+          }, 100);
+        }
+      });
+      // 处理按压事件
+      const handleStart = () => {
+        if(isLandscapeScreen.value){
+          return
+        }
+        plyrState.duration = player.duration
+        plyrState.mediaTimeShow = true
+      };
+      // 处理松开事件
+      const handleEnd = () => {
+        plyrState.mediaTimeShow = false
+        // 暂停的时候调用
+        if(!player.playing){
+          updateProgressStaff(player.currentTime)
+        }
+      };
+      const progressDom = document.querySelector("#playMediaSection .plyr__controls .plyr__progress__container") as HTMLElement
+      progressDom.addEventListener('mousedown', handleStart);
+      progressDom.addEventListener('touchstart', handleStart);
+      progressDom.addEventListener('mouseup', handleEnd);
+      progressDom.addEventListener('touchend', handleEnd);
+    }
+    //点击改变播放状态
+    function handlerClickPlay(event?:MouseEvent){
+      // 原生 播放暂停按钮 点击的时候 不触发
+      // @ts-ignore
+      if(event?.target?.matches('button.plyr__control')){
+        return
+      }
+      if (state._plrl.playing) {
+        state._plrl.pause();
+      } else {
+        state._plrl.play();
+      }
+    }
+    /**
+		 * 音频可视化
+		 * @param audioDom
+		 * @param canvasDom
+		 * @param fftSize  2的幂数,最小为32
+		 */
+    function audioVisualDraw(audioDom: HTMLAudioElement, canvasDom: HTMLCanvasElement, fftSize = 128) {
+      type propsType = { canvWidth: number; canvHeight: number; canvFillColor: string; lineColor: string; lineGap: number }
+      // canvas
+      const canvasCtx = canvasDom.getContext("2d")!
+      let { width, height } = canvasDom.getBoundingClientRect()
+      width = Math.ceil(width)
+      height = Math.ceil(height)
+      canvasDom.width = width
+      canvasDom.height = height
+      // audio
+      // let audioCtx : AudioContext | null = null
+      // let analyser : AnalyserNode | null = null
+      // let source : MediaElementAudioSourceNode | null = null
+      // const dataArray = new Uint8Array(fftSize / 2)
+      const draw = (data: Uint8Array, ctx: CanvasRenderingContext2D, { lineGap, canvWidth, canvHeight, canvFillColor, lineColor }: propsType) => {
+        if (!ctx) return
+        const w = canvWidth
+        const h = canvHeight
+        fillCanvasBackground(ctx, w, h, canvFillColor)
+          // 可视化
+        const dataLen = data.length
+        let step = (w / 2 - lineGap * dataLen) / dataLen
+        step < 1 && (step = 1)
+        const midX = w / 2
+        const midY = h / 2
+        let xLeft = midX
+        for (let i = 0; i < dataLen; i++) {
+          const value = data[i]
+          const percent = value / 255 // 最大值为255
+          const barHeight = percent * midY
+          canvasCtx.fillStyle = lineColor
+          // 中间加间隙
+          if (i === 0) {
+            xLeft -= lineGap / 2
+          }
+          canvasCtx.fillRect(xLeft - step, midY - barHeight, step, barHeight)
+          canvasCtx.fillRect(xLeft - step, midY, step, barHeight)
+          xLeft -= step + lineGap
+        }
+        let xRight = midX
+        for (let i = 0; i < dataLen; i++) {
+          const value = data[i]
+          const percent = value / 255 // 最大值为255
+          const barHeight = percent * midY
+          canvasCtx.fillStyle = lineColor
+          if (i === 0) {
+            xRight += lineGap / 2
+          }
+          canvasCtx.fillRect(xRight, midY - barHeight, step, barHeight)
+          canvasCtx.fillRect(xRight, midY, step, barHeight)
+          xRight += step + lineGap
+        }
+      }
+      const fillCanvasBackground = (ctx: CanvasRenderingContext2D, w: number, h: number, colors: string) => {
+        ctx.clearRect(0, 0, w, h)
+        ctx.fillStyle = colors
+        ctx.fillRect(0, 0, w, h)
+      }
+      const requestAnimationFrameFun = () => {
+        // requestAnimationFrame(() => {
+        //   //analyser?.getByteFrequencyData(dataArray)
+        //   draw(generateMixedData(48), canvasCtx, {
+        //     lineGap: 2,
+        //     canvWidth: width,
+        //     canvHeight: height,
+        //     canvFillColor: "transparent",
+        //     lineColor: "rgba(255, 255, 255, 0.7)"
+        //   })
+        //   if (!isPause) {
+        //     requestAnimationFrameFun()
+        //   }
+        // })
+        const _time = setInterval(() => {
+          if (isPause) {
+            clearInterval(_time)
+            return
+          }
+          //analyser?.getByteFrequencyData(dataArray)
+          draw(generateMixedData(48), canvasCtx, {
+            lineGap: 2,
+            canvWidth: width,
+            canvHeight: height,
+            canvFillColor: "transparent",
+            lineColor: "rgba(255, 255, 255, 0.7)"
+          })
+        }, 300);
+      }
+      let isPause = true
+      const playVisualDraw = () => {
+        // if (!audioCtx) {
+        //   audioCtx = new AudioContext()
+        //   source = audioCtx.createMediaElementSource(audioDom)
+        //   analyser = audioCtx.createAnalyser()
+        //   analyser.fftSize = fftSize
+        //   source?.connect(analyser)
+        //   analyser.connect(audioCtx.destination)
+        // }
+        //audioCtx.resume()  // 重新更新状态   加了暂停和恢复音频音质发生了变化  所以这里取消了
+        isPause = false
+        requestAnimationFrameFun()
+      }
+      const pauseVisualDraw = () => {
+        isPause = true
+        requestAnimationFrame(()=>{
+          canvasCtx.clearRect(0, 0, width, height);
+        })
+        //audioCtx?.suspend()  // 暂停   加了暂停和恢复音频音质发生了变化  所以这里取消了
+        // source?.disconnect()
+        // analyser?.disconnect()
+      }
+      return {
+        playVisualDraw,
+        pauseVisualDraw
+      }
+    }
+    function handlerBack(event:any){
+      event.stopPropagation()
+      verticalScreen()
+    }
+    function landscapeScreen(){
+      postMessage({
+        api: "setRequestedOrientation",
+        content: {
+          orientation: 0,
+        },
+      });
+      isLandscapeScreen.value = true
+    }
+    function verticalScreen(){
+      postMessage({
+        api: "setRequestedOrientation",
+        content: {
+          orientation: 1,
+        },
+      });
+      isLandscapeScreen.value = false
+    }
+    function handlerLandscapeScreen(event:any){
+      event.stopPropagation()
+      if(!isLandscapeScreen.value){
+        landscapeScreen()
+      }
+      // playProgressData.playState = !!state._plrl?.playing
+      // playProgressData.playProgress = state._plrl?.currentTime || 0
+      // router.push({
+      //   path:"/playCreation",
+      //   query:{
+      //     resourceUrl:encodeURIComponent(state.musicDetail?.videoUrl),
+      //     videoBgUrl:encodeURIComponent(state.musicDetail?.videoImg || ""),
+      //     musicSheetName:encodeURIComponent(state.musicDetail?.musicSheetName),
+      //     username:encodeURIComponent(state.musicDetail?.username),
+      //     musicSheetId:encodeURIComponent(state.musicDetail?.musicSheetId),
+      //     speedRate:encodeURIComponent(staffState.speedRate),
+      //     musicRenderType:encodeURIComponent(staffState.musicRenderType),
+      //     partIndex:encodeURIComponent(staffState.partIndex),
+      //   }
+      // })
+    }
+    // 初始化五线谱
+    function initStaff(){
+      // const src = `/klx-music-score/#/simple-detail?id=${state.musicDetail.musicSheetId}&musicRenderType=${staffState.musicRenderType}&part-index=${staffState.partIndex}`;
+      const src = `http://192.168.3.68:3000/instrument.html#/simple-detail?id=${state.musicDetail.musicSheetId}&musicRenderType=${staffState.musicRenderType}&part-index=${staffState.partIndex}`;
+      staffState.staffSrc = src
+      window.addEventListener('message', (event) => {
+        const { api, height } = event.data;
+        if (api === 'api_musicPage') {
+          staffState.isShow = true
+          staffState.height = height + "px"
+        }
+      });
+    }
+
+    function staffMoveInstance(){
+      let isPause = true
+      const requestAnimationFrameFun = () => {
+				requestAnimationFrame(() => {
+          staffDom.value?.contentWindow?.postMessage(
+            {
+              api: 'api_playProgress',
+              content: {
+                currentTime: state._plrl.currentTime * staffState.speedRate
+              }
+            },
+            "*"
+          )
+					if (!isPause) {
+						requestAnimationFrameFun()
+					}
+				})
+			}
+      const playStaff = () => {
+        // 没渲染不执行
+        if(!staffState.isShow) return
+				isPause = false
+        staffDom.value?.contentWindow?.postMessage(
+          {
+            api: 'api_play'
+          },
+          "*"
+        )
+				requestAnimationFrameFun()
+			}
+			const pauseStaff = () => {
+        // 没渲染不执行
+        if(!staffState.isShow) return
+				isPause = true
+        staffDom.value?.contentWindow?.postMessage(
+          {
+            api: 'api_paused'
+          },
+          "*"
+        )
+			}
+      const updateProgressStaff = (currentTime: number) => {
+        // 没渲染不执行
+        if(!staffState.isShow) return
+        staffDom.value?.contentWindow?.postMessage(
+          {
+            api: 'api_updateProgress',
+            content: {
+              currentTime: currentTime * staffState.speedRate
+            }
+          },
+          "*"
+        )
+      }
+			return {
+				playStaff,
+				pauseStaff,
+        updateProgressStaff
+			}
+    }   
     onMounted(async () => {
+      setStatusBarTextColor(true)
       try {
         const res = await api_userMusicDetail(state.id)
         // console.log(res);
@@ -228,7 +507,7 @@ export default defineComponent({
             theme: 'round-button',
             confirmButtonColor: '#2DC7AA'
           }).then(() => {
-            if (browser().isApp) {
+            if (isApp) {
               postMessage({
                 api: 'goBack'
               })
@@ -239,182 +518,194 @@ export default defineComponent({
           return
         }
         state.musicDetail = res.data || {}
+        try{
+          const jsonConfig = JSON.parse(res.data.jsonConfig)
+          jsonConfig.speedRate && (staffState.speedRate = jsonConfig.speedRate)
+          jsonConfig.musicRenderType && (staffState.musicRenderType = jsonConfig.musicRenderType)
+          jsonConfig["part-index"] && (staffState.partIndex = jsonConfig["part-index"])
+        }catch{
+        }     
+        // 五线谱
+        initStaff()   
         getStarList()
         // 判断是视频还是音频
         if (res.data.videoUrl.lastIndexOf('mp4') !== -1) {
           state.playType = 'Video'
         } else {
           state.playType = 'Audio'
-          // 初始化
-          nextTick(() => {
-            initAudio()
-          })
         }
+        // 初始化
+        nextTick(() => {
+          initMediaPlay()
+        })
       } catch {
         //
       }
     })
 
     onUnmounted(() => {
-      if (audioDom) {
-        audioDom.pause()
-        state.paused = audioDom.paused
-      }
+      cleanScrollEvent()
+      state._plrl?.destroy()
     })
     return () => (
-      <div class={styles.creation}>
-        <MSticky position="top">
+      <div
+        style={
+          {
+            '--barheight':state.heightV + "px"
+          }
+        }
+        class={[
+          styles.creation,
+          isTablet && styles.creationTablet,
+          isScreenScroll.value && styles.isScreenScroll
+        ]}>
+        <div class={styles.creationBg}></div>
+        <MSticky position="top"
+          onGetHeight={(height: any) => {
+            console.log(height, 'height', height)
+            state.heightV = height
+          }}
+        >
           <MHeader
+            color={isScreenScroll.value ? "#333333" : "#ffffff"}
+            background={isScreenScroll.value ? `rgb(255,255,255` : "transparent"}
+            title={state.musicDetail?.musicSheetName}
             border={false}
             isBack={route.query.platformType != 'ANALYSIS'}
+            onLeftClick={()=>{ setStatusBarTextColor(false) }}
           />
         </MSticky>
-        <div class={styles.playSection}>
-          {state.playType === 'Video' && (
-            <MVideo
-              src={state.musicDetail?.videoUrl}
-              poster={state.musicDetail?.videoImg || videoBg}
-            />
-          )}
-          {state.playType === 'Audio' && (
-            <div class={styles.audioSection}>
-              <div class={styles.audioContainer}>
-                {/* <div
-                  id={audioId}
-                  onClick={(e: MouseEvent) => {
-                    e.stopPropagation();
-                  }}></div> */}
-                <div
-                  class={styles.waveActive}
-                  style={{
-                    width: state.audioWidth + '%'
-                  }}
-                ></div>
-                <div class={styles.waveDefault}></div>
-              </div>
-
-              <div class={styles.audioBox}>
-                <div
-                  class={[styles.audioPan, state.paused && styles.imgRotate]}
-                >
-                  <Image class={styles.audioImg} src={state.musicDetail?.img} />
-                </div>
-                <i class={styles.audioPoint}></i>
-                <i
-                  class={[styles.audioZhen, state.paused && styles.active]}
-                ></i>
-              </div>
-              <div
-                class={[styles.controls]}
-                onClick={(e: Event) => {
-                  e.stopPropagation()
-                }}
-                onTouchmove={(e: TouchEvent) => {
-                  // emit('close');
-                }}
-              >
-                <div class={styles.actions}>
-                  <div class={styles.actionBtn} onClick={onToggleAudio}>
-                    <img src={state.paused ? iconPlay : iconPause} />
+        <div class={styles.singer}>
+          演奏:{state.musicDetail?.username}
+        </div>        
+        <Sticky zIndex={1000} offsetTop={state.heightV - 1 + "px"}>
+          <div class={[styles.playSection, plyrState.mediaTimeShow && styles.mediaTimeShow, isLandscapeScreen.value&&styles.isLandscapeScreen]} id="playMediaSection" onClick={handlerClickPlay}>
+            {
+              isLandscapeScreen.value &&
+                <div class={styles.backBox}>
+                  <img class={[styles.backImg, state.playType === 'Video' && styles.back1Img]} src={state.playType === 'Video' ? back1Img : backImg} onClick={handlerBack}/>
+                  <div class={styles.musicDetail}>
+                    <div class={styles.musicSheetName}>
+                        <NoticeBar
+                            text={state.musicDetail?.musicSheetName}
+                            background="none"
+                        />
+                    </div>
+                    <div class={styles.username}>演奏:{state.musicDetail?.username}</div>
                   </div>
                 </div>
-                <div class={[styles.slider]}>
-                  <Slider
-                    step={0.01}
-                    class={styles.timeProgress}
-                    v-model={state.currentTime}
-                    max={state.duration}
-                    onUpdate:modelValue={val => {
-                      handleChangeTime(val)
-                    }}
-                    onDrag-start={() => {
-                      state.dragStatus = true
-                      console.log('onDragStart')
-                    }}
-                    onDrag-end={() => {
-                      state.dragStatus = false
-                      console.log('onDragEnd')
-                    }}
-                  />
+            }
+            {
+              state.playType === 'Audio' &&
+              <div class={styles.audioBox}>
+                <canvas class={styles.audioVisualizer} id="audioVisualizer"></canvas>
+                <audio
+                  crossorigin="anonymous"
+                  id="audioMediaSrc"
+                  src={state.musicDetail?.videoUrl}
+                  controls="false"
+                  preload="metadata"
+                  playsinline
+                  webkit-playsinline
+                />
+                <img src="./img/ty.png" class={styles.tyBg} />
+                <div class="audioBoxBg">
+                    <div class={[styles.audioPan,  plyrState.playIngShow && styles.imgRotate]}>
+                      <img class={styles.audioImg} src={state.musicDetail.img || musicBg} />
+                    </div>
+                    <i class={styles.audioPoint}></i>
+                    <i class={[styles.audioZhen, plyrState.playIngShow && styles.active]}></i>
+                </div>                
+              </div>
+            }
+            {
+              state.playType === 'Video' &&
+              <video
+                id="videoMediaSrc"
+                class={styles.videoBox}
+                src={state.musicDetail?.videoUrl}
+                data-poster={ state.musicDetail?.videoImg || videobg}
+                poster={ state.musicDetail?.videoImg || videobg}
+                preload="metadata"
+                playsinline
+                webkit-playsinline
+                x5-playsinline
+              />
+            }
+            <div class={[styles.playLarge, !plyrState.mediaTimeShow && plyrState.playIngShow && styles.playIngShow]}></div>
+            <div class={styles.mediaTimeCon}>
+              <div class={styles.mediaTime}>
+                <div>
+                  {getSecondRPM(plyrState.currentTime)}
                 </div>
-                <div class={styles.time}>
-                  <div>{getSecondRPM(state.currentTime)}</div>
-                  <span>/</span>
-                  <div>{getSecondRPM(state.duration)}</div>
+                <div class={styles.note}>/</div>
+                <div class={styles.duration}>
+                  {getSecondRPM(plyrState.duration)}
                 </div>
               </div>
             </div>
-          )}
-        </div>
-
-        <Cell class={styles.userSection} center border={false}>
-          {{
-            icon: () => (
-              <div
-                class={[
-                  styles.userLogoSection,
-                  (state.musicDetail.vipType === 'SVIP' ||
-                    state.musicDetail.vipType === 'PERMANENT_SVIP') &&
-                    styles.userSVip,
-                  state.musicDetail.vipType === 'VIP' && styles.userVip
-                ]}
-              >
-                <Image class={styles.userLogo} src={state.musicDetail.avatar} />
-                <i class={styles.showMemeber}></i>
+            <div class={styles.landscapeScreen} onClick={handlerLandscapeScreen}></div>
+            {/* 谱面 */}
+            {
+              staffState.staffSrc &&
+              <div class={[styles.staffBoxCon, staffState.isShow && styles.staffBoxShow]}>
+                <div
+                  class={styles.staffBox}
+                  style={
+                    {
+                      '--staffBoxHeight':staffState.height
+                    }
+                  }
+                >
+                  <div class={styles.mask}></div>
+                  <iframe
+                    ref={staffDom}
+                    class={styles.staff}
+                    frameborder="0"
+                    src={staffState.staffSrc}>
+                  </iframe>
+                </div>
               </div>
-            ),
-            title: () => (
-              <div class={styles.userInfo}>
-                <p class={styles.name}>
-                  <span>{state.musicDetail?.username}</span>
-                  {/* {state.musicDetail.vipType === 'VIP' && (
-                    <img src={iconVip} class={styles.iconMember} />
+            }
+          </div>
+        </Sticky>
+
+        <div class={styles.musicSection}>
+          <div class={styles.avatarInfoBox}>
+            <div class={styles.avatar}>
+              <Image class={styles.userLogo} src={state.musicDetail.avatar} />
+              <div class={styles.infoCon}>
+                <div class={styles.info}>
+                  <span class={styles.userName}>{state.musicDetail?.username}</span>
+                  {state.musicDetail.vipFlag && (
+                    <img src={iconMember} class={styles.iconMember} />
                   )}
-                  {(state.musicDetail.vipType === 'SVIP' ||
-                    state.musicDetail.vipType === 'PERMANENT_SVIP') && (
-                    <img src={iconSVip} class={styles.iconMember} />
-                  )} */}
-                </p>
-                <p class={styles.sub}>
+                </div>
+                <div class={styles.sub}>
                   {state.musicDetail.subjectName}{' '}
                   {getGradeCh(state.musicDetail.currentGradeNum - 1)}
-                </p>
-              </div>
-            ),
-            value: () => (
-              <div class={[styles.zan, styles.zanActive]}>
-                <img src={iconZanActive} class={styles.iconZan} />
-                {state.musicDetail.likeNum}
+                </div>
               </div>
-            )
-          }}
-        </Cell>
-
-        <div class={styles.musicSection}>
-          <div class={styles.musicName}>
-            <span class={styles.musicTag}>曲目名称</span>
-            {state.musicDetail?.musicSheetName}
+            </div>
+            <div class={styles.linkes}>
+              <img src={iconZan} class={styles.iconZan} />
+              <span>{state.musicDetail.likeNum}</span>
+            </div>
           </div>
-          {state.musicDetail.desc && (
-            <div class={styles.musicDesc}>{state.musicDetail.desc}</div>
-          )}
+          <TextEllipsis class={styles.textEllipsis} text={state.musicDetail.desc || ''} />
         </div>
-
         <div class={styles.likeSection}>
           <div class={styles.likeTitle}>点赞记录</div>
-
           {state.listState.dataShow ? (
             <List
               finished={state.listState.finished}
               finishedText=" "
-              class={[styles.container, styles.containerInformation]}
               onLoad={getStarList}
-              immediateCheck={false}
-            >
+              immediateCheck={false}>
               {state.list.map((item: any, index: number) => (
                 <Cell
-                  class={styles.likeItem}
-                  border={state.list.length - 1 == index ? false : true}
+                  class={[styles.likeItem, index===state.list.length-1&&styles.likeItemLast]}
+                  border={false}
                 >
                   {{
                     icon: () => (
@@ -439,11 +730,21 @@ export default defineComponent({
               ))}
             </List>
           ) : (
-            <MEmpty btnStatus={false} tips="暂无数据" />
+            <MEmpty class={styles.mEmpty} tips="暂无点赞记录~" btnStatus={false} />
           )}
         </div>
-
-        <MSticky position="bottom">
+        {
+          !isScreenScroll.value &&
+          <MSticky ref={mStickyUpward} position="bottom" offsetBottom={state.heightB - 1 + "px"} >
+            <div class={styles.upward}>
+              <img src={iconUpward} />
+            </div>
+          </MSticky>
+        }
+        <MSticky ref={mStickyBottom} position="bottom" onGetHeight={(height: any) => {
+            console.log(height, 'height', height)
+            state.heightB = height
+        }}>
           <div class={styles.bottomSection}>
             <div class={styles.bottomShare}>
               <p onClick={onDownload}>
@@ -459,21 +760,17 @@ export default defineComponent({
                 <span>删除</span>
               </p>
             </div>
-            <Button
-              round
+            <img src={iconEdit}
               class={styles.btnEdit}
-              type="primary"
               onClick={() => {
                 router.push({
                   path: '/creation-edit',
                   query: {
                     id: state.id
                   }
-                })
+                });
               }}
-            >
-              编辑
-            </Button>
+            />
           </div>
         </MSticky>
 
@@ -482,13 +779,14 @@ export default defineComponent({
           round
           class={styles.popupContainer}
         >
-          <p class={styles.popupContent}>确定删除吗?</p>
+          <p class={styles.popupTit}>温馨提示</p>
+          <p class={styles.popupContent}>确认删除作品吗?</p>
           <div class={styles.popupBtnGroup}>
             <Button round onClick={() => (state.deleteStatus = false)}>
               取消
             </Button>
             <Button round type="primary" onClick={onDelete}>
-              确
+              确
             </Button>
           </div>
         </Popup>

+ 12 - 0
src/views/creation/text-ellipsis/index.module.less

@@ -0,0 +1,12 @@
+.ellipsis {
+  line-height: 0.2rem;
+  &.vis {
+     visibility: hidden;
+  }
+  .ellipsisAct {
+     cursor: pointer;
+     color: #14e0d3;
+     font-weight: 500;
+     margin-left: 2px;
+  }
+}

+ 99 - 0
src/views/creation/text-ellipsis/index.tsx

@@ -0,0 +1,99 @@
+import { defineComponent } from 'vue'
+import styles from './index.module.less'
+
+export default defineComponent({
+    name: "TextEllipsis",
+    data() {
+       return {
+          ellipsisData: {
+             oversize: false,
+             computedReady: false, // 先隐形计算,计算好后,再根据配置显示
+             expanded: false,
+             key: 0
+          }
+       }
+    },
+    props: {
+       text: {
+          type: String
+       },
+       lines: {
+          type: Number,
+          default: 2 // 默认显示的行数
+       }
+    },
+    watch: {
+       text() {
+          // 强制刷新组件
+          this.ellipsisData.key++
+          this.ellipsisData.expanded = false
+          this.computeText()
+       }
+    },
+    created() {
+       this.computeText()
+    },
+    methods: {
+       getStyle(el, styleName) {
+          if (!el) return ""
+          if (styleName === "float") {
+             styleName = "cssFloat"
+          }
+          try {
+             const style = el.style[styleName]
+             if (style) return style
+             const computed = document.defaultView?.getComputedStyle(el, "")
+             return computed ? computed[styleName] : ""
+          } catch {
+             return el.style[styleName]
+          }
+       },
+       computeText() {
+          this.ellipsisData.oversize = false
+          this.ellipsisData.computedReady = false
+          this.$nextTick(() => {
+             let text: any = this.text
+             let height = 0
+             const lines = this.lines
+             const ellipsisDom: any = this.$refs.ellipsisDom
+             const textDom: any = this.$refs.textDom
+             if (this.ellipsisData.expanded) {
+                textDom.innerText = this.text
+                this.ellipsisData.oversize = true
+                this.ellipsisData.computedReady = true
+                return
+             }
+             if (!height && lines) {
+                const lineHeight = parseInt(this.getStyle(ellipsisDom, "lineHeight"))
+                height = lineHeight * lines
+             }
+             if (ellipsisDom.offsetHeight > height) {
+                this.ellipsisData.oversize = true
+                this.$nextTick(() => {
+                   while (ellipsisDom.offsetHeight > height) {
+                      if (ellipsisDom.offsetHeight > height * 3) {
+                         textDom.innerText = text = text.substring(0, Math.floor(text.length / 2))
+                      } else {
+                         textDom.innerText = text = text.substring(0, text.length - 1)
+                      }
+                   }
+                })
+             }
+             this.ellipsisData.computedReady = true
+          })
+       },
+       handleExpand() {
+          this.ellipsisData.expanded = !this.ellipsisData.expanded
+          this.computeText()
+       }
+    },
+    render() {
+        return (
+            <div ref="ellipsisDom" class={[styles.ellipsis, !this.ellipsisData.computedReady && styles.vis]} key={this.ellipsisData.key}>
+                <span ref="textDom">{ this.text }</span>
+                <span v-show="ellipsisData.oversize && !ellipsisData.expanded">...</span>
+                <span v-show="ellipsisData.oversize" class="ellipsisAct" onClick={this.handleExpand}>{ this.ellipsisData.expanded ? "收起" : "展开" }</span>
+            </div>
+        )
+    }
+})