| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- 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/icon-back.svg";
- 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/shiyi-close.svg";
- import firstLeft from "./image/first-left.svg";
- import firstRight from "./image/first-right.svg";
- import firstTop from "./image/first-top.svg";
- import firstBottom from "./image/first-bottom.svg";
- import firstCorrect from "./image/first-correct.svg";
- import firstError from "./image/first-error.svg";
- import firstNot from "./image/first-not.svg";
- import firstLack from "./image/first-lack.svg";
- 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";
- 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<any>()
- const lottieDom1 = ref<any>()
- const lottieDom2 = ref<any>()
- const level: any = {
- BEGINNER: "入门级",
- ADVANCED: "进阶级",
- PERFORMER: "大师级",
- };
- // console.log("🚀 ~ scoreData:", scoreData.value)
- const itemType = ref<IItemType>("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 () => (
- <div class={[styles.headerTop, browserInfo.android && styles.android]}>
- <div class={styles.left}>
- <div class={[styles.back, !storeData.isApp && styles.disabled]} onClick={handleBack}>
- <img src={iconBack} />
- </div>
- <div class={styles.leftContent}>
- {/* <div class={styles.lcName}>{state.examSongName}</div> */}
- <Title class={styles.lcName} text={state.examSongName} rightView={false} />
- <div class={styles.lcScore}>{level[scoreData.value.heardLevel]}|综合分数:{scoreData.value.score}分</div>
- </div>
- </div>
- {/* 音准、节奏、完整度纬度 */}
-
- <div class={styles.middle}>
- {
- state.isPercussion ? null :
- <div
- onClick={() => handleChange("intonation")}
- class={[styles.cItem, 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, 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, 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}
- 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' ?
- <>
- {state.isPercussion ? null : (
- <div class={styles.demos}>
- {itemType.value === "intonation" && (
- <>
- <div>
- <Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-2} y={0} />
- <span>演奏偏高</span>
- </div>
- <div>
- <Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-1} y={-3} />
- <span>演奏偏低</span>
- </div>
- </>
- )}
- {itemType.value === "cadence" && (
- <>
- <div>
- <Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={0.5} y={-1} />
- <span>节奏偏快</span>
- </div>
- <div>
- <Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-3} y={-2.5} />
- <span>演奏偏低</span>
- </div>
- </>
- )}
- {(itemType.value === "intonation" || itemType.value === "cadence") && (
- <>
- <div>
- <Note fill="#2ABC6F" />
- <span>演奏正确</span>
- </div>
- <div>
- <Note fill="#FF2B29" />
- <span>演奏错误</span>
- </div>
- </>
- )}
- {(itemType.value === "intonation" || itemType.value === "integrity") && (
- <div>
- <Note fill="#8F4EFB" />
- <span>时值不足</span>
- </div>
- )}
- {
- itemType.value === "integrity" &&
- <div>
- <Note fill="#2ABC6F" />
- <span>时值正确</span>
- </div>
- }
- <div>
- <Note fill="#ADADAD" />
- <span>未演奏</span>
- </div>
- </div>
- )}
- </> :
- <>
- {state.isPercussion ? null : (
- <div class={styles.demos}>
- {itemType.value === "intonation" && (
- <>
- <div>
- <img class={styles.firstIcon1} src={firstTop} />
- <span>演奏偏高</span>
- </div>
- <div>
- <img class={styles.firstIcon1} src={firstBottom} />
- <span>演奏偏低</span>
- </div>
- </>
- )}
- {itemType.value === "cadence" && (
- <>
- <div>
- <img class={styles.firstIcon2} src={firstLeft} />
- <span>节奏偏快</span>
- </div>
- <div>
- <img class={styles.firstIcon2} src={firstRight} />
- <span>节奏偏慢</span>
- </div>
- </>
- )}
- {(itemType.value === "intonation" || itemType.value === "cadence") && (
- <>
- <div>
- <img class={styles.firstIcon3} src={firstCorrect} />
- <span>演奏正确</span>
- </div>
- <div>
- <img class={styles.firstIcon3} src={firstError} />
- <span>演奏错误</span>
- </div>
- </>
- )}
- {(itemType.value === "intonation" || itemType.value === "integrity") && (
- <div>
- <img class={styles.firstIcon3} src={firstLack} />
- <span>时值不足</span>
- </div>
- )}
- {
- itemType.value === "integrity" &&
- <div>
- <img class={styles.firstIcon3} src={firstCorrect} />
- <span>时值正确</span>
- </div>
- }
- <div>
- <img class={styles.firstIcon3} src={firstNot} />
- <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} />
- <span>黄色音符在上:演奏偏高</span>
- </div>
- <div class={styles.item}>
- <Note fill="#2ABC6F" />
- <span>绿色音符:演奏/时值正确</span>
- </div>
- <div class={styles.item}>
- <Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-1} y={-3} />
- <span>黄色音符在下:演奏偏低</span>
- </div>
- <div class={styles.item}>
- <Note fill="#FF2B29" />
- <span>红色音符:演奏错误</span>
- </div>
- <div class={styles.item}>
- <Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={0.5} y={-1} />
- <span>黄色音符在左:节奏偏快</span>
- </div>
- <div class={styles.item}>
- <Note fill="#8F4EFB" />
- <span>紫色音符:时值不足</span>
- </div>
- <div class={styles.item}>
- <Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-3} y={-2.5} />
- <span>黄色音符在右:节奏偏慢</span>
- </div>
- <div class={styles.item}>
- <Note fill="#ADADAD" />
- <span>灰色音符:未演奏</span>
- </div>
- </div>
- </div> :
- <div class={styles.shiyiPopup}>
- <img class={styles.shiyiTop} src={shiyiTop} />
- <div class={styles.items}>
- <div class={styles.itemTone}>
- <img class={styles.firstIcon1} src={firstTop} />
- <span>黄色箭头朝上:演奏偏高</span>
- </div>
- <div class={styles.itemTone}>
- <img class={styles.firstIcon3} src={firstCorrect} />
- <span>绿色音符:演奏/时值正确</span>
- </div>
- <div class={styles.itemTone}>
- <img class={styles.firstIcon1} src={firstBottom} />
- <span>黄色箭头朝下:演奏偏低</span>
- </div>
- <div class={styles.itemTone}>
- <img class={styles.firstIcon3} src={firstError} />
- <span>红色音符:演奏错误</span>
- </div>
- <div class={styles.itemTone}>
- <img class={[styles.firstIcon2, styles.fiz]} src={firstLeft} />
- <span>黄色箭头朝左:节奏偏快</span>
- </div>
- <div class={styles.itemTone}>
- <img class={styles.firstIcon3} src={firstLack} />
- <span>紫色音符:时值不足</span>
- </div>
- <div class={styles.itemTone}>
- <img class={styles.firstIcon2} src={firstRight} />
- <span>黄色箭头朝右:节奏偏慢</span>
- </div>
- <div class={styles.itemTone}>
- <img class={styles.firstIcon3} src={firstNot} />
- <span>灰色音符:未演奏</span>
- </div>
- </div>
- </div>
- }
- </Popup>
- </div>
- );
- },
- });
|