| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765 | import {  defineComponent,  nextTick,  onMounted,  onUnmounted,  reactive,  watch,  ref} from 'vue';// import WaveSurfer from 'wavesurfer.js';import styles from './index.module.less';import MSticky from '@/components/m-sticky';import MHeader from '@/components/m-header';import {  Button,  Cell,  Image,  List,  Popup,  Slider,  showDialog,  showToast,  Sticky,  TextEllipsis} from 'vant';import iconDownload from './images/icon-download.png';import iconShare from './images/icon-share.png';import iconDelete from './images/icon-delete.png';import iconEdit from './images/edit.png';import iconUpward from './images/upward.png';import iconMember from './images/icon-member.png';import iconZan from './images/icon-zan.png';import promptImg from './images/prompt.png';import confirmImg from './images/confirm.png';import canceImg from './images/cance.png';import iconZanActive from './images/icon-zan-active.png';import iconPlay from './images/icon-play.png';import iconPause from './images/icon-pause.png';import { postMessage, promisefiyPostMessage } from '@/helpers/native-message';import { browser, getGradeCh, getSecondRPM, vaildMusicScoreUrl } from '@/helpers/utils';import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';import {  api_userMusicDetail,  api_userMusicRemove,  api_userMusicStarPage} from './api';import MEmpty from '@/components/m-empty';import dayjs from 'dayjs';import MVideo from '@/components/m-video';import ShareModel from './share-model';import { usePageVisibility, useEventListener } from '@vant/use';import "plyr/dist/plyr.css";import Plyr from "plyr";import { Vue3Lottie } from "vue3-lottie";import audioBga from "./images/audioBga.json";import audioBga1 from "./images/leftCloud.json";import audioBga2 from "./images/rightCloud.json";import videobg from "./images/videobg.png";import playProgressData from "./playCreation/playProgress"import Loading from './loading';export default defineComponent({  name: 'creation-detail',  setup() {    const route = useRoute();    const router = useRouter();    const isScreenScroll = ref(false)    const lottieDom = ref()    const lottieDom1 = ref()    const lottieDom2 = ref()    const mStickyBottom = ref()    const mStickyUpward = ref()    const state = reactive({      id: route.query.id,      deleteStatus: false,      shareStatus: false,      playType: '' as 'Audio' | 'Video' | '', // 播放类型      musicDetail: {} as any,      isClick: false,      list: [] as any,      listState: {        dataShow: true, // 判断是否有数据        loading: false,        finished: false      },      params: {        page: 1,        rows: 20      },      _plrl: null as any,      heightV:0,      heightB:0    });    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 staffDom= ref<HTMLIFrameElement>()    const {playStaff, pauseStaff, updateProgressStaff} = staffMoveInstance()    // 获取列表    const getStarList = async () => {      try {        if (state.isClick) return;        state.isClick = true;        const res = await api_userMusicStarPage({          userMusicId: state.id,          ...state.params        });        state.listState.loading = false;        const result = res.data || {};        // 处理重复请求数据        if (state.list.length > 0 && result.current === 1) {          return;        }        state.list = state.list.concat(result.rows || []);        state.listState.finished = result.current >= result.pages;        state.params.page = result.current + 1;        state.listState.dataShow = state.list.length > 0;        state.isClick = false;      } catch {        state.listState.dataShow = false;        state.listState.finished = true;        state.isClick = false;      }    };    // 删除作品    const onDelete = async () => {      try {        await api_userMusicRemove({ id: state.id });        setTimeout(() => {          state.deleteStatus = false;          showToast('删除成功');        }, 100);        setTimeout(() => {          if (browser().isApp) {            postMessage({              api: 'goBack'            });          } else {            router.back();          }        }, 1200);      } catch {        //      }    };    // 下载    const onDownload = async () => {      await promisefiyPostMessage({        api: 'saveFile',        content: {          url: state.musicDetail.videoUrl        }      });    };    // 滚动事件    const cleanScrollEvent = 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 }      })    }    // 初始化 媒体播放    function initMediaPlay(){      const id = state.playType === "Audio" ? "#audioMediaSrc" : "#videoMediaSrc";      state._plrl = new Plyr(id, {        controls: ["progress"],        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', () => {          lottieDom.value?.play()          lottieDom1.value?.play()          lottieDom2.value?.play()          playVisualDraw()        });        player.on('pause', () => {          lottieDom.value?.pause()          lottieDom1.value?.pause()          lottieDom2.value?.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 = () => {        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(){      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")!      const { width, height } = canvasDom.getBoundingClientRect()      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(dataArray, canvasCtx, {            lineGap: 2,            canvWidth: width,            canvHeight: height,            canvFillColor: "transparent",            lineColor: "rgba(255, 255, 255, 0.7)"          })          if (!isPause) {            requestAnimationFrameFun()          }        })      }      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        //audioCtx?.suspend()  // 暂停   加了暂停和恢复音频音质发生了变化  所以这里取消了        // source?.disconnect()        // analyser?.disconnect()      }      return {        playVisualDraw,        pauseVisualDraw      }    }    function handlerLandscapeScreen(event:any){      event.stopPropagation()      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 = `${vaildMusicScoreUrl()}/instrument/#/simple-detail?id=${state.musicDetail.musicSheetId}&musicRenderType=${staffState.musicRenderType}&part-index=${staffState.partIndex}`;      //const src = `http://192.168.3.122: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"          // 如果是播放中自动开始播放  不是播放 自动跳转到当前位置          if(playProgressData.playState){            handlerClickPlay()          }else{            updateProgressStaff(state._plrl.currentTime)          }        }      });    }    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);        if (res.code === 999) {          showDialog({            message: res.message,            theme: 'round-button',            confirmButtonColor:              'linear-gradient(73deg, #5BECFF 0%, #259CFE 100%)'          }).then(() => {            if (browser().isApp) {              postMessage({                api: 'goBack'              });            } else {              router.back();            }          });          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.partIndex && (staffState.partIndex = jsonConfig.partIndex)        }catch{        }        // 五线谱        initStaff()        getStarList();        // 判断是视频还是音频        if (res.data.videoUrl.lastIndexOf('mp4') !== -1) {          state.playType = 'Video';        } else {          state.playType = 'Audio';        }        nextTick(()=>{          initMediaPlay()        })      } catch {        //      }      requestAnimationFrame(()=>{        mStickyBottom.value?.onChnageHeight()        mStickyBottom.value?.onChnageHeight()      })    });    onUnmounted(() => {      setStatusBarTextColor(false)      cleanScrollEvent()      state._plrl?.destroy()    });    onBeforeRouteLeave((to, from, next)=>{      if(to.path !== "/playCreation"){        playProgressData.playProgress = 0        playProgressData.playState = false      }      next()    })    return () => (      <div        style={          {            '--barheight':state.heightV + "px"          }        }        class={[          styles.creation,          browser().isTablet && styles.creationTablet,          isScreenScroll.value && styles.isScreenScroll        ]}>        <div class={styles.creationBg}></div>        <MSticky position="top"          onBarHeight={(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'}          />        </MSticky>        <div class={styles.singer}>          演奏:{state.musicDetail?.username}        </div>        <Sticky offsetTop={state.heightV - 1 + "px"}>          <div class={[styles.playSection, plyrState.mediaTimeShow && styles.mediaTimeShow]} id="playMediaSection" onClick={handlerClickPlay}>            {              state.playType === 'Audio' &&              <div class={styles.audioBox}>                <canvas class={styles.audioVisualizer} id="audioVisualizer"></canvas>                <Vue3Lottie ref={lottieDom} class={styles.audioBga} animationData={audioBga} autoPlay={false} loop={true}></Vue3Lottie>                <Vue3Lottie ref={lottieDom1} class={styles.audioBga1} animationData={audioBga1} autoPlay={false} loop={true}></Vue3Lottie>                <Vue3Lottie ref={lottieDom2} class={styles.audioBga2} animationData={audioBga2} autoPlay={false} loop={true}></Vue3Lottie>                <audio                  crossorigin="anonymous"                  id="audioMediaSrc"                  src={state.musicDetail?.videoUrl}                  controls="false"                  preload="metadata"                  playsinline                  webkit-playsinline                />              </div>            }            {              state.playType === 'Video' &&              <video                id="videoMediaSrc"                class={styles.videoBox}                src={state.musicDetail?.videoUrl}                data-poster={ state.musicDetail?.videoImg || videobg}                preload="metadata"                playsinline              />            }            <div class={[styles.playLarge, plyrState.playIngShow && styles.playIngShow]}></div>            <div class={styles.mediaTime}>              <div>                {getSecondRPM(plyrState.currentTime)}              </div>              <div class={styles.note}>/</div>              <div class={styles.duration}>                {getSecondRPM(plyrState.duration)}              </div>            </div>            <div class={styles.landscapeScreen} onClick={handlerLandscapeScreen}></div>            {/* 谱面 */}            {              staffState.staffSrc &&              <div                class={[styles.staffBox, staffState.isShow && styles.staffBoxShow]}                style={                  {                    '--staffBoxHeight':staffState.height                  }                }              >                <div class={styles.mask}></div>                <iframe                  ref={staffDom}                  class={styles.staff}                  frameborder="0"                  src={staffState.staffSrc}>                </iframe>              </div>            }          </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} />                  )}                </div>                <div class={styles.sub}>                  {state.musicDetail.subjectName}{' '}                  {getGradeCh(state.musicDetail.currentGradeNum - 1)}                </div>              </div>            </div>            <div class={styles.linkes}>              <img src={iconZan} class={styles.iconZan} />              <span>{state.musicDetail.likeNum}</span>            </div>          </div>          <TextEllipsis class={styles.textEllipsis} rows={2} content={state.musicDetail?.desc} expand-text="展开" collapse-text="收起" />        </div>        <div class={styles.likeSection}>          <div class={styles.likeTitle}>点赞记录</div>          {state.listState.dataShow ? (            <List              finished={state.listState.finished}              finishedText=" "              onLoad={getStarList}              immediateCheck={false}>              {state.list.map((item: any, index: number) => (                <Cell                  class={[styles.likeItem, index===state.list.length-1&&styles.likeItemLast]}                  border={false}                >                  {{                    icon: () => (                      <Image src={item.userAvatar} class={styles.userLogo} />                    ),                    title: () => (                      <div class={styles.userInfo}>                        <p class={styles.name}>{item.userName}</p>                        <p class={styles.sub}>                          {item.subjectName}{' '}                          {getGradeCh(item.currentGradeNum - 1)}                        </p>                      </div>                    ),                    value: () => (                      <div class={styles.time}>                        {dayjs(item.createTime).format('YYYY-MM-DD HH:mm')}                      </div>                    )                  }}                </Cell>              ))}            </List>          ) : (            <MEmpty class={styles.mEmpty} image={"empty2"} description="暂无点赞记录" />          )}        </div>        {          !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" onBarHeight={(height: any) => {            console.log(height, 'height', height)            state.heightB = height        }}>          <div class={styles.bottomSection}>            <div class={styles.bottomShare}>              <p onClick={onDownload}>                <img src={iconDownload} />                <span>下载</span>              </p>              <p onClick={() => (state.shareStatus = true)}>                <img src={iconShare} />                <span>分享</span>              </p>              <p onClick={() => (state.deleteStatus = true)}>                <img src={iconDelete} />                <span>删除</span>              </p>            </div>            <img src={iconEdit}              class={styles.btnEdit}              onClick={() => {                router.push({                  path: '/creation-edit',                  query: {                    id: state.id                  }                });              }}            />          </div>        </MSticky>        <Popup          v-model:show={state.deleteStatus}          overlay-style={            {              backgroundColor:"rgba(0,0,0,.5)"            }          }          round          class={styles.popupContainer}>            <img class={styles.prompt} src={promptImg} />            <div class={styles.deleteBox}>              <p class={styles.popupContent}>确定删除吗?</p>              <div class={styles.popupBtnGroup}>                <img src={canceImg} onClick={() => (state.deleteStatus = false)} />                <img src={confirmImg} onClick={onDelete} />              </div>            </div>        </Popup>        <Popup          position="bottom"          v-model:show={state.shareStatus}          style={{ background: 'transparent' }}>          <ShareModel            playType={state.playType}            musicDetail={state.musicDetail}            onClose={() => (state.shareStatus = false)}          />        </Popup>        {          !staffState.isShow && <Loading></Loading>        }      </div>    );  }});
 |