import { computed, defineComponent, nextTick, reactive, ref, toRefs } from "vue"; import styles from "./index.module.less"; import { api_back } from "/src/helpers/communication"; import state from "/src/state"; import iconBack from "./image/back_icon.png"; import iconShiyi from "./image/icon-shiyi.svg"; import iconhuifang from "./image/icon-huifang.svg"; import shiyiTop from "./image/shiyi-top.png"; import shiyiClose from "./image/closeImg.png"; import { Grid, GridItem, Popup } from "vant"; import videobg from "./image/videobg.png"; import "plyr/dist/plyr.css"; import Plyr from "plyr"; import { browser } from "/src/utils"; import Note from "../note"; import { storeData } from "/src/store"; import Title from "/src/page-instrument/header-top/title"; import { Vue3Lottie } from "vue3-lottie"; import audioBga from "./image/audioBga.json"; import audioBga1 from "./image/leftCloud.json"; import audioBga2 from "./image/rightCloud.json"; import { EvaluatingReportDriver } from "/src/page-instrument/custom-plugins/guide-driver"; // 音准、节奏、完整度 type IItemType = "intonation" | "cadence" | "integrity"; export default defineComponent({ name: "header-top", props: { scoreData: { type: Object, default: () => ({}), }, }, setup(props, { expose }) { const browserInfo = browser(); const { scoreData } = toRefs(props); const shareData = reactive({ show: false, shiyiShow: false, isInitPlyr: false, _plrl: null as any, }); const lottieDom = ref(); const lottieDom1 = ref(); const lottieDom2 = ref(); const level: any = { BEGINNER: "入门级", ADVANCED: "进阶级", PERFORMER: "大师级", }; // 颜色配置 const bgColors = { high: "#FF66A6", low: "#FFB900", right: "#65FFAE", wrong: "#DA3736", lack: "#A5CBFF", not: "#FFFFFF", fast: "#B366FF", slow: "#FF7B00", }; // console.log("🚀 ~ scoreData:", scoreData.value) const itemType = ref(state.isPercussion ? "cadence" : "intonation"); /** 返回 */ const handleBack = () => { api_back(); }; const handleChange = (type: IItemType) => { itemType.value = type; scoreData.value.itemType = type; }; // 资源类型 const mediaType = computed((): "audio" | "video" => { const subfix = (scoreData.value.videoFilePath || "").split(".").pop(); if (subfix === "wav" || subfix === "mp3" || subfix === "m4a") { return "audio"; } return "video"; }); const openAudioAndVideo = () => { shareData.show = true; if (shareData.isInitPlyr) return; nextTick(() => { const id = mediaType.value === "audio" ? "#audioSrc" : "#videoSrc"; shareData._plrl = new Plyr(id, { controls: ["play-large", "play", "progress", "current-time", "duration"], fullscreen: { enabled: false }, }); // 创建音波数据 if(mediaType.value === "audio"){ setTimeout(() => { const audioDom = document.querySelector("#audioSrc") as HTMLAudioElement const canvasDom = document.querySelector("#audioVisualizer") as HTMLCanvasElement const { pauseVisualDraw, playVisualDraw } = audioVisualDraw(audioDom, canvasDom) shareData._plrl.on('play', () => { lottieDom.value?.play() lottieDom1.value?.play() lottieDom2.value?.play() playVisualDraw() }); shareData._plrl.on('pause', () => { lottieDom.value?.pause() lottieDom1.value?.pause() lottieDom2.value?.pause() pauseVisualDraw() }); }, 300); // 弹窗动画是0.25秒 这里用定时器 确保canvas 能获取到宽高 } shareData.isInitPlyr = true; }); }; /** * 音频可视化 * @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.3)" }) 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 } } return () => ( <>
{/*
{state.examSongName}
*/} <div class={styles.lcScore}> {level[scoreData.value.heardLevel]}|速度:{scoreData.value.speed}|综合分数:{scoreData.value.score}分 </div> </div> </div> {/* 音准、节奏、完整度纬度 */} <div class={styles.middle}> {state.isPercussion ? null : ( <div onClick={() => handleChange("intonation")} class={[styles.cItem, "evaluting-report-1", itemType.value === "intonation" && styles.active]}> <span class={styles.mScore}>{scoreData.value.intonation}分</span> <span class={styles.mLabel}>音准</span> </div> )} <div onClick={() => handleChange("cadence")} class={[styles.cItem, "evaluting-report-2", itemType.value === "cadence" && styles.active]}> <span class={styles.mScore}>{scoreData.value.cadence}分</span> <span class={styles.mLabel}>节奏</span> </div> {state.isPercussion ? null : ( <div onClick={() => handleChange("integrity")} class={[styles.cItem, "evaluting-report-3", itemType.value === "integrity" && styles.active]}> <span class={styles.mScore}>{scoreData.value.integrity}分</span> <span class={styles.mLabel}>完成度</span> </div> )} </div> {/* <div class={styles.center}> <div class={styles.cItem}> <div>{level[scoreData.value.heardLevel]}</div> <div>难度</div> </div> <div class={styles.cItem}> <div>{scoreData.value.score}分</div> <div>评测分数</div> </div> {state.isPercussion ? null : ( <> <div onClick={() => handleChange("intonation")} class={[styles.cItem, itemType.value === "intonation" && styles.active]} > <div style={{ color: "rgb(45, 199, 170)" }}>{scoreData.value.intonation}分</div> <div>音准</div> </div> <div onClick={() => handleChange("cadence")} class={[styles.cItem, itemType.value === "cadence" && styles.active]} > <div style={{ color: "#FF4E19" }}>{scoreData.value.cadence}分</div> <div>节奏</div> </div> <div onClick={() => handleChange("integrity")} class={[styles.cItem, itemType.value === "integrity" && styles.active]} > <div style={{ color: "rgb(255, 196, 89)" }}>{scoreData.value.integrity}分</div> <div>完成度</div> </div> </> )} </div> */} <div class={styles.right}> <div style={{ display: scoreData.value.videoFilePath ? "" : "none" }} class={[styles.btn, "evaluting-report-4"]} onClick={openAudioAndVideo}> <img class={styles.iconBtn} src={iconhuifang} /> <span>回放</span> </div> <div class={styles.btn} onClick={() => (shareData.shiyiShow = true)}> <img class={styles.iconBtn} src={iconShiyi} /> <span>释义</span> </div> {/* <div class={styles.btn}> <img class={styles.iconBtn} src={iconhuifang} /> <span>再来一遍</span> </div> */} </div> {/* 五线谱,简谱类型提示 */} {scoreData.value.musicType === "staff" ? ( <> {( <div class={styles.demos}> {itemType.value === "intonation" && ( <> <div> {/* <Note fill="rgba(255, 102, 166, 1)" shadowFill="#FFAB25" shadow x={-2} y={0} /> */} <Note fill="#FF66A6" /> <span>演奏偏高</span> </div> <div> <Note fill="#FFB900" /> <span>演奏偏低</span> </div> </> )} {itemType.value === "cadence" && ( <> <div> <Note fill="#B366FF" /> <span>节奏偏快</span> </div> <div> <Note fill="#FF7B00" /> <span>节奏偏慢</span> </div> </> )} {(itemType.value === "intonation" || itemType.value === "cadence") && ( <> <div> <Note fill="#65FFAE" /> <span>演奏正确</span> </div> <div> <Note fill="#DA3736" /> <span>演奏错误</span> </div> </> )} {itemType.value === "integrity" && ( <div> <Note fill="#A5CBFF" /> <span>时值不足</span> </div> )} {itemType.value === "integrity" && ( <div> <Note fill="#65FFAE" /> <span>演奏正确</span> </div> )} <div> <Note fill="#FFFFFF" /> <span>未演奏</span> </div> </div> )} </> ) : ( <> {( <div class={styles.demos}> {itemType.value === "intonation" && ( <> <div> {/* <img class={styles.firstIcon1} src={firstTop} /> */} <i style={{ background: bgColors.high }}></i> <span>演奏偏高</span> </div> <div> <i style={{ background: bgColors.low }}></i> <span>演奏偏低</span> </div> </> )} {itemType.value === "cadence" && ( <> <div> <i style={{ background: bgColors.fast }}></i> <span>节奏偏快</span> </div> <div> <i style={{ background: bgColors.slow }}></i> <span>节奏偏慢</span> </div> </> )} {(itemType.value === "intonation" || itemType.value === "cadence") && ( <> <div> <i style={{ background: bgColors.right }}></i> <span>演奏正确</span> </div> <div> <i style={{ background: bgColors.wrong }}></i> <span>演奏错误</span> </div> </> )} {itemType.value === "integrity" && ( <div> <i style={{ background: bgColors.lack }}></i> <span>时值不足</span> </div> )} {itemType.value === "integrity" && ( <div> <i style={{ background: bgColors.right }}></i> <span>演奏正确</span> </div> )} <div> <i style={{ background: bgColors.not }}></i> <span>未演奏</span> </div> </div> )} </> )} <Popup teleport="body" class={["popup-custom", "van-scale", styles.popup]} transition="van-scale" v-model:show={shareData.show} closeable onClose={() => { shareData._plrl?.pause(); }} > <div class={styles.playerBox}> {mediaType.value === "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="audioSrc" src={scoreData.value.videoFilePath} controls="false" preload="metadata" playsinline /> </div> ) : ( <video id="videoSrc" class={styles.videoBox} src={scoreData.value.videoFilePath} data-poster={videobg} preload="metadata" playsinline /> )} </div> </Popup> <Popup v-model:show={shareData.shiyiShow} class="popup-custom van-scale center-closeBtn shiyiBox" transition="van-scale" teleport="body" closeable> <img onClick={() => (shareData.shiyiShow = false)} class={styles.shiyiClose} src={shiyiClose} /> {scoreData.value.musicType === "staff" ? ( <div class={styles.shiyiPopup}> <img class={styles.shiyiTop} src={shiyiTop} /> <div class={styles.items}> <div class={styles.item}> {/* <Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-2} y={0} /> */} <Note fill="#FF66A6" /> <span>玫红色音符:演奏偏高</span> </div> <div class={styles.item}> <Note fill="#4BED98" /> <span>绿色音符:演奏正确</span> </div> <div class={styles.item}> <Note fill="#FFB900" /> <span>黄色音符:演奏偏低</span> </div> <div class={styles.item}> <Note fill="#DA3736" /> <span>红色音符:演奏错误</span> </div> <div class={styles.item}> <Note fill="#B366FF" /> <span>紫色音符:节奏偏快</span> </div> <div class={styles.item}> <Note fill="#A5CBFF" /> <span>浅蓝色音符:时值不足</span> </div> <div class={styles.item}> <Note fill="#FF7B00" /> <span>橙色音符:节奏偏慢</span> </div> <div class={styles.item}> <Note fill="#FFFFFF" /> <span>白色音符:未演奏</span> </div> </div> </div> ) : ( <div class={styles.shiyiPopup}> <img class={styles.shiyiTop} src={shiyiTop} /> <div class={styles.items}> <div class={styles.itemTone}> <i style={{ background: bgColors.high }}></i> <span>玫红色音符:演奏偏高</span> </div> <div class={styles.itemTone}> <i style={{ background: bgColors.right }}></i> <span>绿色音符:演奏正确</span> </div> <div class={styles.itemTone}> <i style={{ background: bgColors.low }}></i> <span>黄色音符:演奏偏低</span> </div> <div class={styles.itemTone}> <i style={{ background: bgColors.wrong }}></i> <span>红色音符:演奏错误</span> </div> <div class={styles.itemTone}> <i style={{ background: bgColors.fast }}></i> <span>紫色音符:节奏偏快</span> </div> <div class={styles.itemTone}> <i style={{ background: bgColors.lack }}></i> <span>浅蓝色音符:时值不足</span> </div> <div class={styles.itemTone}> <i style={{ background: bgColors.slow }}></i> <span>橙色音符:节奏偏慢</span> </div> <div class={styles.itemTone}> <i style={{ background: bgColors.not }}></i> <span>白色音符:未演奏</span> </div> </div> </div> )} </Popup> </div> <EvaluatingReportDriver videoFilePath={scoreData.value.videoFilePath} /> </> ); }, });