lex 1 year ago
parent
commit
817dc688a3

+ 11 - 0
package-lock.json

@@ -19,6 +19,7 @@
         "plyr": "^3.7.8",
         "query-string": "^8.1.0",
         "umi-request": "^1.4.0",
+        "vudio.js": "^1.0.3",
         "vue": "^3.3.4",
         "vue-router": "^4.1.6",
         "vue3-lottie": "^2.7.0",
@@ -8128,6 +8129,11 @@
         "vue": "^3.0.0"
       }
     },
+    "node_modules/vudio.js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/vudio.js/-/vudio.js-1.0.3.tgz",
+      "integrity": "sha512-pip5HpUUmRYTKblq9EoDwUaKSTD4O+8LbGpzKYxRt6ZVQLzS/SpmjvsMTb8Wut6DzAn5fYzxifb9xzRZwEI2IQ=="
+    },
     "node_modules/vue": {
       "version": "3.3.4",
       "resolved": "https://registry.npmmirror.com/vue/-/vue-3.3.4.tgz",
@@ -14766,6 +14772,11 @@
         "evtd": "^0.2.2"
       }
     },
+    "vudio.js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/vudio.js/-/vudio.js-1.0.3.tgz",
+      "integrity": "sha512-pip5HpUUmRYTKblq9EoDwUaKSTD4O+8LbGpzKYxRt6ZVQLzS/SpmjvsMTb8Wut6DzAn5fYzxifb9xzRZwEI2IQ=="
+    },
     "vue": {
       "version": "3.3.4",
       "resolved": "https://registry.npmmirror.com/vue/-/vue-3.3.4.tgz",

+ 1 - 0
package.json

@@ -32,6 +32,7 @@
     "plyr": "^3.7.8",
     "query-string": "^8.1.0",
     "umi-request": "^1.4.0",
+    "vudio.js": "^1.0.3",
     "vue": "^3.3.4",
     "vue-router": "^4.1.6",
     "vue3-lottie": "^2.7.0",

+ 2 - 0
src/shims-vue.d.ts

@@ -3,3 +3,5 @@ declare module '*.vue' {
   const component: DefineComponent<{}, {}, any>;
   export default component;
 }
+
+declare module 'vudio.js';

+ 155 - 0
src/views/attend-class/component/audio-pay copy.tsx

@@ -0,0 +1,155 @@
+import { defineComponent, onMounted, toRefs, reactive } from 'vue';
+import styles from './audio.module.less';
+import WaveSurfer from 'wavesurfer.js';
+import iconplay from '../image/icon-pause.svg';
+import iconpause from '../image/icon-play.svg';
+import { NSlider } from 'naive-ui';
+
+export default defineComponent({
+  name: 'audio-play',
+  props: {
+    item: {
+      type: Object,
+      default: () => {
+        return {};
+      }
+    },
+    isEmtry: {
+      type: Boolean,
+      default: false
+    }
+  },
+  setup(props) {
+    const { item } = toRefs(props);
+    const audioId = 'a' + +Date.now() + Math.floor(Math.random() * 100);
+    const audioForms = reactive({
+      paused: true,
+      currentTimeNum: 0,
+      currentTime: '00:00',
+      duration: '00:00'
+    });
+    const audioDom = new Audio();
+    audioDom.controls = true;
+    audioDom.style.width = '100%';
+    audioDom.className = styles.audio;
+    document.querySelector(`#${audioId}`)?.appendChild(audioDom);
+
+    onMounted(() => {
+      const wavesurfer = WaveSurfer.create({
+        container: document.querySelector(`#${audioId}`) as HTMLElement,
+        waveColor: '#C5C5C5',
+        progressColor: '#02baff',
+        url: item.value.content + '?t=' + +new Date(),
+        cursorWidth: 0,
+        height: 160,
+        normalize: true,
+        // Set a bar width
+        barWidth: 6,
+        // Optionally, specify the spacing between bars
+        barGap: 12,
+        // And the bar radius
+        barRadius: 12,
+        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', () => {
+        audioForms.paused = audioDom.paused;
+        audioForms.duration = timeFormat(Math.round(audioDom.duration));
+      });
+
+      wavesurfer.on('finish', () => {
+        audioForms.paused = true;
+      });
+    });
+
+    // 切换音频播放
+    const onToggleAudio = (e: MouseEvent) => {
+      e.stopPropagation();
+      if (audioDom.paused) {
+        audioDom.play();
+      } else {
+        audioDom.pause();
+      }
+
+      audioForms.paused = audioDom.paused;
+    };
+
+    // 播放时监听
+    audioDom.addEventListener('timeupdate', () => {
+      audioForms.currentTime = timeFormat(Math.round(audioDom.currentTime));
+      audioForms.currentTimeNum = audioDom.currentTime;
+    });
+
+    // 播放结束时
+
+    // 对时间进行格式化
+    const timeFormat = (num: number) => {
+      if (num > 0) {
+        const m = Math.floor(num / 60);
+        const s = num % 60;
+        return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
+      } else {
+        return '00:00';
+      }
+    };
+
+    return () => (
+      <div class={styles.audioWrap}>
+        <div class={styles.audioContainer}>
+          <div
+            id={audioId}
+            onClick={(e: MouseEvent) => {
+              e.stopPropagation();
+            }}></div>
+        </div>
+
+        <div
+          class={styles.controls}
+          onClick={(e: MouseEvent) => {
+            e.stopPropagation();
+          }}>
+          <div class={styles.actions}>
+            <div class={styles.actionWrap}>
+              <button class={styles.actionBtn} onClick={onToggleAudio}>
+                {audioForms.paused ? (
+                  <img class={styles.playIcon} src={iconplay} />
+                ) : (
+                  <img class={styles.playIcon} src={iconpause} />
+                )}
+              </button>
+            </div>
+            <div class={styles.time}>
+              <div
+                class="plyr__time plyr__time--current"
+                aria-label="Current time">
+                {audioForms.currentTime}
+              </div>
+              <span class={styles.line}>/</span>
+              <div
+                class="plyr__time plyr__time--duration"
+                aria-label="Duration">
+                {audioForms.duration}
+              </div>
+            </div>
+          </div>
+
+          <div class={styles.slider}>
+            <NSlider
+              v-model:value={audioForms.currentTimeNum}
+              step={0.01}
+              max={audioDom.duration}
+              tooltip={false}
+            />
+          </div>
+        </div>
+      </div>
+    );
+  }
+});

+ 133 - 65
src/views/attend-class/component/audio-pay.tsx

@@ -1,9 +1,18 @@
-import { defineComponent, onMounted, toRefs, reactive } from 'vue';
+import {
+  defineComponent,
+  toRefs,
+  reactive,
+  ref,
+  nextTick,
+  onMounted,
+  watch
+} from 'vue';
 import styles from './audio.module.less';
-import WaveSurfer from 'wavesurfer.js';
 import iconplay from '../image/icon-pause.svg';
 import iconpause from '../image/icon-play.svg';
 import { NSlider } from 'naive-ui';
+import Vudio from 'vudio.js';
+import tickMp3 from '../image/tick.mp3';
 
 export default defineComponent({
   name: 'audio-play',
@@ -19,75 +28,57 @@ export default defineComponent({
       default: false
     }
   },
-  setup(props) {
-    const { item } = toRefs(props);
-    const audioId = 'a' + +Date.now() + Math.floor(Math.random() * 100);
+  emits: ['loadedmetadata', 'togglePlay', 'ended'],
+  setup(props, { emit, expose }) {
     const audioForms = reactive({
       paused: true,
       currentTimeNum: 0,
       currentTime: '00:00',
-      duration: '00:00'
-    });
-    const audioDom = new Audio();
-    audioDom.controls = true;
-    audioDom.style.width = '100%';
-    audioDom.className = styles.audio;
-    document.querySelector(`#${audioId}`)?.appendChild(audioDom);
-
-    onMounted(() => {
-      const wavesurfer = WaveSurfer.create({
-        container: document.querySelector(`#${audioId}`) as HTMLElement,
-        waveColor: '#C5C5C5',
-        progressColor: '#02baff',
-        url: item.value.content + '?t=' + +new Date(),
-        cursorWidth: 0,
-        height: 160,
-        normalize: true,
-        // Set a bar width
-        barWidth: 6,
-        // Optionally, specify the spacing between bars
-        barGap: 12,
-        // And the bar radius
-        barRadius: 12,
-        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', () => {
-        audioForms.paused = audioDom.paused;
-        audioForms.duration = timeFormat(Math.round(audioDom.duration));
-      });
-
-      wavesurfer.on('finish', () => {
-        audioForms.paused = true;
-      });
+      durationNum: 0,
+      duration: '00:00',
+      showBar: true,
+      afterMa3: true
     });
+    const canvas: any = ref();
+    const audio: any = ref();
+    let vudio: any = null;
 
     // 切换音频播放
     const onToggleAudio = (e: MouseEvent) => {
       e.stopPropagation();
-      if (audioDom.paused) {
-        audioDom.play();
+      if (audio.value.paused) {
+        onInit(audio.value, canvas.value);
+        audio.value.play();
+        audioForms.afterMa3 = false;
       } else {
-        audioDom.pause();
+        audio.value.pause();
       }
+      audioForms.paused = audio.value.paused;
 
-      audioForms.paused = audioDom.paused;
+      emit('togglePlay', audioForms.paused);
     };
 
-    // 播放时监听
-    audioDom.addEventListener('timeupdate', () => {
-      audioForms.currentTime = timeFormat(Math.round(audioDom.currentTime));
-      audioForms.currentTimeNum = audioDom.currentTime;
-    });
-
-    // 播放结束时
+    const onInit = (audio: undefined, canvas: undefined) => {
+      if (!vudio) {
+        vudio = new Vudio(audio, canvas, {
+          effect: 'waveform',
+          accuracy: 256,
+          width: 1024,
+          height: 600,
+          waveform: {
+            maxHeight: 200,
+            color: [
+              [0, '#44D1FF'],
+              [0.5, '#44D1FF'],
+              [0.5, '#198CFE'],
+              [1, '#198CFE']
+            ],
+            prettify: false
+          }
+        });
+        vudio.dance();
+      }
+    };
 
     // 对时间进行格式化
     const timeFormat = (num: number) => {
@@ -100,18 +91,90 @@ export default defineComponent({
       }
     };
 
+    //
+    const toggleHideControl = (isShow: false) => {
+      audioForms.showBar = isShow;
+    };
+
+    let vudio1 = null;
+    const canvas1: any = ref();
+    const audio1: any = ref();
+    nextTick(() => {
+      vudio1 = new Vudio(audio1.value, canvas1.value, {
+        effect: 'waveform',
+        accuracy: 256,
+        width: 1024,
+        height: 600,
+        waveform: {
+          maxHeight: 200,
+          color: [
+            [0, '#44D1FF'],
+            [0.5, '#44D1FF'],
+            [0.5, '#198CFE'],
+            [1, '#198CFE']
+          ],
+          prettify: false
+        }
+      });
+      vudio1.dance();
+    });
+    expose({
+      toggleHideControl
+    });
+
     return () => (
       <div class={styles.audioWrap}>
         <div class={styles.audioContainer}>
-          <div
-            id={audioId}
-            onClick={(e: MouseEvent) => {
-              e.stopPropagation();
-            }}></div>
+          <audio
+            ref={audio}
+            crossorigin="anonymous"
+            src={props.item.content + '?time=1'}
+            onEnded={() => {
+              audioForms.paused = true;
+              emit('ended');
+            }}
+            onTimeupdate={() => {
+              audioForms.currentTime = timeFormat(
+                Math.round(audio.value?.currentTime || 0)
+              );
+              audioForms.currentTimeNum = audio.value.currentTime;
+            }}
+            onLoadedmetadata={() => {
+              audioForms.duration = timeFormat(
+                Math.round(audio.value.duration)
+              );
+              audioForms.durationNum = audio.value.duration;
+
+              if (props.item.autoPlay && audio.value) {
+                audio.value.play();
+              }
+              audio.value.stop = () => {
+                audio.value.pause();
+                audioForms.paused = true;
+              };
+              audio.value.onPlay = () => {
+                audio.value.play();
+                audioForms.paused = false;
+              };
+
+              emit('loadedmetadata', audio.value);
+            }}></audio>
+
+          <canvas ref={canvas}></canvas>
+
+          {audioForms.afterMa3 && (
+            <div class={styles.tempVudio}>
+              <audio ref={audio1} src={tickMp3} />
+              <canvas ref={canvas1}></canvas>
+            </div>
+          )}
         </div>
 
         <div
-          class={styles.controls}
+          class={[
+            styles.controls,
+            audioForms.showBar ? '' : styles.sectionAnimate
+          ]}
           onClick={(e: MouseEvent) => {
             e.stopPropagation();
           }}>
@@ -142,10 +205,15 @@ export default defineComponent({
 
           <div class={styles.slider}>
             <NSlider
-              v-model:value={audioForms.currentTimeNum}
+              value={audioForms.currentTimeNum}
               step={0.01}
-              max={audioDom.duration}
+              max={audioForms.durationNum}
               tooltip={false}
+              onUpdate:value={(val: number) => {
+                audio.value.currentTime = val;
+                audioForms.currentTimeNum = val;
+                audioForms.currentTime = timeFormat(Math.round(val || 0));
+              }}
             />
           </div>
         </div>

+ 22 - 0
src/views/attend-class/component/audio.module.less

@@ -21,6 +21,20 @@
     top: 0;
     opacity: 0;
   }
+
+  .tempVudio {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    padding: 0 240px;
+  }
+
+  canvas {
+    width: 100%;
+    height: 100%;
+  }
 }
 
 .controls {
@@ -36,6 +50,7 @@
   transition: all 0.5s;
   display: flex;
   align-items: center;
+  transition: all .5s;
 
   .time {
     display: flex;
@@ -99,4 +114,11 @@
       transition: all .2s;
     }
   }
+}
+
+.sectionAnimate {
+  opacity: 0;
+  pointer-events: none;
+  transform: translateY(100%);
+  transition: all .5s;
 }

+ 1 - 1
src/views/attend-class/component/musicScore.tsx

@@ -33,7 +33,7 @@ export default defineComponent({
     const isLoaded = ref(false);
     const renderError = ref(false);
     const renderSuccess = ref(false);
-    const src = `https://test.lexiaoya.cn/orchestra-music-score/?_t=1687590480955&id=11707&modelType=practice&modeType=json&Authorization=bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyLXJlc291cmNlIl0sImNsaWVudFR5cGUiOiJCQUNLRU5EIiwidXNlcl9uYW1lIjoiMTgxNjI4NTU4MDAiLCJzY29wZSI6WyJhbGwiXSwidXNlcklkIjoiMTAwMDE0OSIsImF1dGhvcml0aWVzIjpbIjE4MTYyODU1ODAwIl0sImp0aSI6IjY0MzA2NjllLTE5NGItNDk3Yy1hMDQ5LWM4YWUxMGU0NDNiOCIsImNsaWVudF9pZCI6ImptZWR1LWJhY2tlbmQifQ.aeJ0o-lSfx1Ok-YptZuC-AUY6M7k3LK9rr0Bmx7sj81pPt2HDiDqeT2PuriYdJacxWnxboyhdG_lwU0QK-W-vON97c45NQpSEFLVpZ0m1xdIqwllwf20xhyj5YJwdOFUzxf1TNEfGsHZg66J7wEJQBSzlmQwcxmEE5lqLVD8o_3f2SBqnWCj9RqE4air7FUemllMnZiu8HsS-TKtLDaGa_XW8Yb_Zjzzz6r5UcYNI-C1uKPXg18o1dhHBJ8O-Pl0U8WivPRub_opvO2NSn5L9YtPt7Dd50UeSwaIOdMeCGdii1bg__h77Stek1_5IaZLYkoo2gvmUA-xk09TwCQBpA`;
+    const src = `https://dev.kt.colexiu.com/instrument`;
     const checkView = () => {
       fetch(src)
         .then(() => {

BIN
src/views/attend-class/image/tick.mp3


+ 79 - 17
src/views/attend-class/index.tsx

@@ -54,6 +54,7 @@ export default defineComponent({
       if (activeItem.type != 'VIDEO') return;
       if (value == 'hidden') {
         isPlay.value = !activeItem.videoEle?.paused;
+        isPlay.value = !activeItem.audioEle?.paused;
         togglePlay(activeItem, false);
       } else {
         // 页面显示,并且
@@ -95,6 +96,7 @@ export default defineComponent({
       isCourse: false,
       isRecordPlay: false,
       videoRefs: {} as any[],
+      audioRefs: {} as any[],
       modelAttendStatus: false, // 布置作业提示弹窗
       modelTrainStatus: false // 训练设置
     });
@@ -129,26 +131,26 @@ export default defineComponent({
     // };
     const getDetail = async () => {
       data.knowledgePointList = [
-        // {
-        //   id: '5',
-        //   name: '歌曲表演 大鹿',
-        //   title: '歌曲表演 大鹿',
-        //   type: 'AUDIO',
-        //   content:
-        //     'https://cloud-coach.ks3-cn-beijing.ksyuncs.com/1686819360752.mp3',
-        //   url: 'https://lanhu-dds-backend.oss-cn-beijing.aliyuncs.com/merge_image/imgs/23cc71b5d7874dcf8752cd257483e687_mergeImage.png'
-        // },
         {
-          id: '1',
-          name: '其多列',
-          title: '其多列',
-          type: 'VIDEO',
+          id: '5',
+          name: '歌曲表演 大鹿',
+          title: '歌曲表演 大鹿',
+          type: 'AUDIO',
           content:
-            'https://gyt.ks3-cn-beijing.ksyuncs.com/courseware/1687844560120.mp4',
-          url: 'https://gyt.ks3-cn-beijing.ksyuncs.com/courseware/1687844640957.png'
+            'https://cloud-coach.ks3-cn-beijing.ksyuncs.com/1686819360752.mp3',
+          url: 'https://lanhu-dds-backend.oss-cn-beijing.aliyuncs.com/merge_image/imgs/23cc71b5d7874dcf8752cd257483e687_mergeImage.png'
         },
         // {
         //   id: '1',
+        //   name: '其多列',
+        //   title: '其多列',
+        //   type: 'VIDEO',
+        //   content:
+        //     'https://gyt.ks3-cn-beijing.ksyuncs.com/courseware/1687844560120.mp4',
+        //   url: 'https://gyt.ks3-cn-beijing.ksyuncs.com/courseware/1687844640957.png'
+        // },
+        // {
+        //   id: '1',
         //   name: '歌曲表演 大鹿',
         //   title: '歌曲表演 大鹿',
         //   type: 'VIDEO',
@@ -186,6 +188,7 @@ export default defineComponent({
           ...m,
           iframeRef: null,
           videoEle: null,
+          audioEle: null,
           autoPlay: false, //加载完成是否自动播放
           isprepare: false, // 视频是否加载完成
           isRender: false // 是否渲染了
@@ -231,9 +234,14 @@ export default defineComponent({
     const handleStop = () => {
       for (let i = 0; i < data.itemList.length; i++) {
         const activeItem = data.itemList[i];
+        console.log(activeItem, 'activeItem');
         if (activeItem.type === 'VIDEO' && activeItem.videoEle) {
           activeItem.videoEle.stop();
         }
+
+        if (activeItem.type === 'AUDIO' && activeItem.audioEle) {
+          activeItem.audioEle.stop();
+        }
         // console.log('🚀 ~ activeItem:', activeItem)
         // 停止曲谱的播放
         if (activeItem.type === 'SONG') {
@@ -260,6 +268,10 @@ export default defineComponent({
         Object.values(data.videoRefs).map((n: any) =>
           n.toggleHideControl(false)
         );
+
+        Object.values(data.audioRefs).map((n: any) =>
+          n.toggleHideControl(false)
+        );
       }, 4000);
     };
 
@@ -269,10 +281,12 @@ export default defineComponent({
       message.destroyAll();
       activeData.model = false;
       Object.values(data.videoRefs).map((n: any) => n.toggleHideControl(false));
+      Object.values(data.audioRefs).map((n: any) => n.toggleHideControl(false));
     };
     const toggleModel = (type = true) => {
       activeData.model = type;
       Object.values(data.videoRefs).map((n: any) => n.toggleHideControl(type));
+      Object.values(data.audioRefs).map((n: any) => n.toggleHideControl(type));
     };
 
     // 双击
@@ -381,6 +395,15 @@ export default defineComponent({
             if (item.type == 'SONG') {
               activeData.model = true;
             }
+            if (item.type === 'AUDIO') {
+              // 自动播放下一个音频
+              clearTimeout(activeData.timer);
+              message.destroyAll();
+              item.autoPlay = true;
+              nextTick(() => {
+                item.audioEle?.onPlay();
+              });
+            }
             if (item.type === 'VIDEO') {
               // 自动播放下一个视频
               clearTimeout(activeData.timer);
@@ -439,11 +462,15 @@ export default defineComponent({
       if (item?.type == 'VIDEO' && !item.videoEle?.paused) {
         setModelOpen();
       }
+
+      if (item?.type == 'AUDIO' && !item.audioEle?.paused) {
+        setModelOpen();
+      }
     };
 
     // 监听页面键盘事件 - 上下切换
     document.body.addEventListener('keyup', (e: KeyboardEvent) => {
-      console.log(e, 'e');
+      // console.log(e, 'e');
       if (e.code === 'ArrowUp') {
         if (popupData.activeIndex === 0) return;
         handlePreAndNext('up');
@@ -469,6 +496,9 @@ export default defineComponent({
       if (activeItem.type === 'VIDEO' && activeItem.videoEle) {
         activeItem.videoEle.pause();
       }
+      if (activeItem.type === 'AUDIO' && activeItem.audioEle) {
+        activeItem.audioEle.stop();
+      }
       if (activeItem.type === 'SONG') {
         activeItem.iframeRef?.contentWindow?.postMessage(
           { api: 'setPlayState' },
@@ -513,6 +543,9 @@ export default defineComponent({
             Object.values(data.videoRefs).map((n: any) =>
               n.toggleHideControl(activeData.model)
             );
+            Object.values(data.audioRefs).map((n: any) =>
+              n.toggleHideControl(activeData.model)
+            );
           }}>
           <div
             class={styles.coursewarePlay}
@@ -560,6 +593,9 @@ export default defineComponent({
                         Object.values(data.videoRefs).map((n: any) =>
                           n.toggleHideControl(activeData.model)
                         );
+                        Object.values(data.audioRefs).map((n: any) =>
+                          n.toggleHideControl(activeData.model)
+                        );
                         if (activeData.model) {
                           setModelOpen();
                         }
@@ -607,7 +643,33 @@ export default defineComponent({
                     ) : m.type === 'IMG' ? (
                       <img src={m.content} />
                     ) : m.type === 'AUDIO' ? (
-                      <AudioPay item={m} />
+                      <AudioPay
+                        item={m}
+                        ref={(v: any) => (data.audioRefs[mIndex] = v)}
+                        onLoadedmetadata={(audioItem: any) => {
+                          m.audioEle = audioItem;
+                          m.isprepare = true;
+                        }}
+                        onTogglePlay={(paused: boolean) => {
+                          m.autoPlay = false;
+                          if (paused || popupData.open) {
+                            clearTimeout(activeData.timer);
+                          } else {
+                            setModelOpen();
+                          }
+                        }}
+                        onEnded={() => {
+                          const _index = popupData.activeIndex + 1;
+                          if (_index < data.itemList.length) {
+                            handleSwipeChange(_index);
+                          }
+                        }}
+                        // onReset={() => {
+                        //   if (!m.audioEle?.paused) {
+                        //     setModelOpen();
+                        //   }
+                        // }}
+                      />
                     ) : (
                       <MusicScore
                         activeModel={activeData.model}