Selaa lähdekoodia

Merge branch 'hqyDev' of http://git.dayaedu.com/lex/classroom-instruments into jenkins

黄琪勇 1 vuosi sitten
vanhempi
commit
77a9d4e447
47 muutettua tiedostoa jossa 1111 lisäystä ja 7 poistoa
  1. 800 0
      src/components/Metronome/Metronome.vue
  2. 214 0
      src/components/Metronome/MetronomeBox.vue
  3. BIN
      src/components/Metronome/audio/tick.wav
  4. BIN
      src/components/Metronome/audio/tock.wav
  5. BIN
      src/components/Metronome/imgs/a1.png
  6. BIN
      src/components/Metronome/imgs/a2.png
  7. BIN
      src/components/Metronome/imgs/a3.png
  8. BIN
      src/components/Metronome/imgs/a4.png
  9. BIN
      src/components/Metronome/imgs/a5.png
  10. BIN
      src/components/Metronome/imgs/a6.png
  11. BIN
      src/components/Metronome/imgs/a7.png
  12. BIN
      src/components/Metronome/imgs/b1.png
  13. BIN
      src/components/Metronome/imgs/b2.png
  14. BIN
      src/components/Metronome/imgs/b3.png
  15. BIN
      src/components/Metronome/imgs/b4.png
  16. BIN
      src/components/Metronome/imgs/b5.png
  17. BIN
      src/components/Metronome/imgs/b6.png
  18. BIN
      src/components/Metronome/imgs/b7.png
  19. BIN
      src/components/Metronome/imgs/bai.png
  20. BIN
      src/components/Metronome/imgs/bg.png
  21. BIN
      src/components/Metronome/imgs/c1.png
  22. BIN
      src/components/Metronome/imgs/c2.png
  23. BIN
      src/components/Metronome/imgs/c3.png
  24. BIN
      src/components/Metronome/imgs/c4.png
  25. BIN
      src/components/Metronome/imgs/c5.png
  26. BIN
      src/components/Metronome/imgs/c6.png
  27. BIN
      src/components/Metronome/imgs/c7.png
  28. BIN
      src/components/Metronome/imgs/close.png
  29. BIN
      src/components/Metronome/imgs/closeMet.png
  30. BIN
      src/components/Metronome/imgs/dot.png
  31. BIN
      src/components/Metronome/imgs/dotBtn.png
  32. BIN
      src/components/Metronome/imgs/jia.png
  33. BIN
      src/components/Metronome/imgs/jian.png
  34. BIN
      src/components/Metronome/imgs/minMet.png
  35. BIN
      src/components/Metronome/imgs/paly.png
  36. BIN
      src/components/Metronome/imgs/pause.png
  37. BIN
      src/components/Metronome/imgs/pauseBtn.png
  38. BIN
      src/components/Metronome/imgs/playtBtn.png
  39. BIN
      src/components/Metronome/imgs/setting.png
  40. BIN
      src/components/Metronome/imgs/sound.png
  41. BIN
      src/components/Metronome/imgs/yin.png
  42. BIN
      src/components/Metronome/imgs/yuan.png
  43. BIN
      src/components/Metronome/imgs/zhen.png
  44. 2 0
      src/components/Metronome/index.ts
  45. 68 0
      src/components/Metronome/useMetronome.ts
  46. 19 3
      src/components/layout/index.tsx
  47. 8 4
      src/views/attend-class/index.tsx

+ 800 - 0
src/components/Metronome/Metronome.vue

@@ -0,0 +1,800 @@
+<template>
+  <div class="Metronome">
+    <div class="MetronomeBox">
+      <div class="headTools">
+        <img
+          src="./imgs/minMet.png"
+          @click="
+            () => {
+              emits('windowMet');
+            }
+          "
+        />
+        <img
+          src="./imgs/closeMet.png"
+          @click="
+            () => {
+              emits('closeMet');
+            }
+          "
+        />
+      </div>
+      <div class="beatWrap">
+        <div class="dot" ref="dotDom" @mousedown="handleDotMousedown"></div>
+        <div class="round"></div>
+        <canvas
+          ref="mycanvasDom"
+          class="mycanvas"
+          width="252"
+          height="252"
+          @click="handleclickCanvas"
+        ></canvas>
+        <div class="beatCon">
+          <img
+            class="optImg"
+            src="./imgs/jia.png"
+            @click="
+              () => {
+                if (speedNum < 200) {
+                  speedNum++;
+                }
+              }
+            "
+          />
+          <div class="optMid">
+            <img class="optImg" src="./imgs/yin.png" />
+            <n-input-number
+              placeholder=""
+              :value="speedNum"
+              :show-button="false"
+              @update:value="(num:number)=>{
+                console.log(num)
+                if(num){
+                  if(num<=50){
+                    speedNum=50
+                  }else if(num>=200){
+                    speedNum=200
+                  }else{
+                    speedNum=num
+                  }
+                }
+              }"
+            />
+          </div>
+          <img
+            class="optImg"
+            src="./imgs/jian.png"
+            @click="
+              () => {
+                if (speedNum > 50) {
+                  speedNum--;
+                }
+              }
+            "
+          />
+        </div>
+      </div>
+      <div class="selectCon">
+        <n-select
+          v-model:value="beatVal"
+          :options="beatValOpt"
+          :show-checkmark="false"
+          :show-arrow="false"
+          :placement="'top'"
+          :render-tag="
+            ({ option }:any) => {
+              return h('div',{class:'beatName'},[h('div',option.label),h('div','拍')])
+            }
+          "
+        ></n-select>
+        <n-select
+          class="beatSymbolSel"
+          v-model:value="beatSymbol"
+          :options="beatSymbolOpt"
+          :show-checkmark="false"
+          :show-arrow="false"
+          :placement="'top'"
+          :render-tag="
+            ({ option }:any) => {
+              return h('div', { class: `beatSymbolImg${option.label} beatSymbolImg` });
+            }
+          "
+          :render-label="
+            (option:any) => {
+              return h('div', { class: `beatSymbolImg${option.label} beatSymbolImg` });
+            }
+          "
+        ></n-select>
+      </div>
+      <div class="sliderList">
+        <img src="./imgs/sound.png" alt="" />
+        <NSlider v-model:value="volumeNum" :step="1" :tooltip="false">
+          <template #thumb>
+            <img class="thumbDot" src="./imgs/dotBtn.png" alt="" />
+          </template>
+        </NSlider>
+        <span class="sliderText">{{ volumeNum }}</span>
+      </div>
+      <div v-if="playState === 'pause'" class="playBtn" @click="startPlay">
+        <span>播放</span>
+        <img src="./imgs/playtBtn.png" />
+      </div>
+      <div v-else class="playBtn" @click="pausePlay">
+        <span>暂停</span>
+        <img class="pauseImg" src="./imgs/pauseBtn.png" />
+      </div>
+    </div>
+    <Dragbom></Dragbom>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, h, watch } from 'vue';
+import { NInputNumber, NSelect, NSlider } from 'naive-ui';
+import useMetronome from './useMetronome';
+import Dragbom from '@/hooks/useDrag/dragbom';
+
+const emits = defineEmits<{
+  (e: 'windowMet'): void;
+  (e: 'closeMet'): void;
+  (e: 'speedNumChange', num: number): void;
+}>();
+
+/* dom */
+const mycanvasDom = ref<HTMLCanvasElement>();
+const dotDom = ref<HTMLElement>();
+/* select */
+const beatVal = ref('1');
+const beatValOpt = [
+  {
+    label: '1',
+    value: '1',
+    class: 'beatValOptItem'
+  },
+  {
+    label: '2',
+    value: '1-1',
+    class: 'beatValOptItem'
+  },
+  {
+    label: '3',
+    value: '1-1-1',
+    class: 'beatValOptItem'
+  },
+  {
+    label: '4',
+    value: '1-1-1-1',
+    class: 'beatValOptItem'
+  },
+  {
+    label: '5',
+    value: '1-1-1-1-1',
+    class: 'beatValOptItem'
+  },
+  {
+    label: '6',
+    value: '1-1-1-1-1-1',
+    class: 'beatValOptItem'
+  },
+  {
+    label: '7',
+    value: '1-1-1-1-1-1-1',
+    class: 'beatValOptItem'
+  },
+  {
+    label: '8',
+    value: '1-1-1-1-1-1-1-1',
+    class: 'beatValOptItem'
+  },
+  {
+    label: '9',
+    value: '1-1-1-1-1-1-1-1-1',
+    class: 'beatValOptItem'
+  }
+];
+const beatSymbol = ref('1');
+const beatSymbolOpt = [
+  {
+    label: '1',
+    value: '1',
+    class: 'beatSymbolOptItem'
+  },
+  {
+    label: '2',
+    value: '0.5-0.5',
+    class: 'beatSymbolOptItem'
+  },
+  {
+    label: '3',
+    value: '0.3333333-0.3333333-0.3333333',
+    class: 'beatSymbolOptItem'
+  },
+  {
+    label: '4',
+    value: '0.25-0.25-0.25-0.25',
+    class: 'beatSymbolOptItem'
+  },
+  {
+    label: '5',
+    value: '0.6666666-0.3333333',
+    class: 'beatSymbolOptItem'
+  },
+  {
+    label: '6',
+    value: '0.75-0.25',
+    class: 'beatSymbolOptItem'
+  },
+  {
+    label: '7',
+    value: '0.5-0.25-0.25',
+    class: 'beatSymbolOptItem'
+  }
+];
+/* 拖动状态 */
+const preInfo = reactive({
+  preBoundary: 0,
+  offsetX: 0,
+  isMin: false,
+  isMax: false
+});
+
+const { volumeNum, playState, speedNum, startPlay, pausePlay } = useMetronome(
+  beatVal,
+  beatSymbol
+);
+
+onMounted(() => {
+  getCircleBar(speedToScalc(speedNum.value));
+});
+watch(speedNum, () => {
+  if (playState.value === 'play') {
+    pausePlay();
+  }
+  getCircleBar(speedToScalc(speedNum.value));
+  emits('speedNumChange', speedNum.value);
+});
+watch([beatVal, beatSymbol], () => {
+  if (playState.value === 'play') {
+    pausePlay();
+  }
+});
+function speedToScalc(speed: number) {
+  return ((speed - 50) / 150) * 100;
+}
+function scalcToSpeed(scalc: number) {
+  return Math.round((scalc * 150) / 100 + 50);
+}
+const handleclickCanvas = (e: MouseEvent) => {
+  const eleRect = mycanvasDom.value!.getBoundingClientRect();
+  const widthDot = eleRect.width;
+  const heightDot = eleRect.height;
+  let scalc = 0;
+  const offsetX = e.offsetX;
+  const offsetY = e.offsetY;
+  if (offsetX > widthDot / 2) {
+    // 开始算百分比
+    scalc = (offsetY / heightDot) * 50;
+  } else {
+    scalc = ((1 - offsetY / heightDot) / 2) * 100 + 50;
+  }
+  speedNum.value = scalcToSpeed(scalc);
+};
+function handleDotMousedown() {
+  function onMouseup() {
+    mycanvasDom.value!.removeEventListener('mousemove', onMousemove);
+    document.removeEventListener('mouseup', onMouseup);
+    preInfo.preBoundary = 0;
+    preInfo.offsetX = 0;
+    preInfo.isMin = false;
+    preInfo.isMax = false;
+  }
+  preInfo.preBoundary = 0;
+  mycanvasDom.value!.addEventListener('mousemove', onMousemove);
+  document.addEventListener('mouseup', onMouseup);
+}
+function onMousemove(e: MouseEvent) {
+  // 中心点 是 130 14
+  const eleRect = mycanvasDom.value!.getBoundingClientRect();
+  const widthDot = eleRect.width;
+  const heightDot = eleRect.height;
+  let scalc = 0;
+  const offsetX = e.offsetX;
+  const offsetY = e.offsetY;
+  // 这里在判断一下
+  const isRight = offsetX - preInfo.offsetX > 0;
+  if (offsetX > widthDot / 2) {
+    // 开始算百分比
+    scalc = (offsetY / heightDot) * 50;
+  } else {
+    scalc = ((1 - offsetY / heightDot) / 2) * 100 + 50;
+  }
+  //  先判断往左 往右
+  if (isRight) {
+    if (scalc - preInfo.preBoundary < -90) {
+      scalc = 100;
+      preInfo.isMax = true;
+      speedNum.value = scalcToSpeed(scalc);
+      return;
+    }
+  } else {
+    // 往左
+    if (scalc - preInfo.preBoundary > 90) {
+      if (scalc > 75 && preInfo.isMin) {
+        return;
+      }
+      preInfo.isMin = true;
+      scalc = 0;
+      speedNum.value = scalcToSpeed(scalc);
+      return;
+    }
+  }
+  if (preInfo.isMin && scalc > 75) {
+    return;
+  }
+  if (preInfo.isMin && scalc < 75) {
+    preInfo.isMin = false;
+    return;
+  }
+  if (preInfo.isMax && scalc < 25) {
+    return;
+  }
+  if (preInfo.isMax && scalc > 25) {
+    preInfo.isMax = false;
+    return;
+  }
+  speedNum.value = scalcToSpeed(scalc);
+  preInfo.preBoundary = scalc;
+  preInfo.offsetX = offsetX;
+}
+// 根据百分比画圆
+function getCircleBar(steps: number) {
+  const radius = 121; //半径
+  const colorList = ['#51DBFF', '#009FFF'];
+  const mycanvas = mycanvasDom.value!;
+  const ctx = mycanvas.getContext('2d')!;
+  // 每次进来清空画布
+  ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
+  //找到画布的中心点
+  const canvasX = mycanvas.width / 2;
+  const canvasY = mycanvas.height / 2;
+  //进度条是100%,所以要把一圈360度分成100份
+  const progress = (Math.PI * 2) / 100;
+  const ao = -Math.PI / 2 + steps * progress;
+  const x1 = radius + Math.cos(ao) * radius;
+  const y1 = radius + Math.sin(ao) * radius;
+  // 这里开始算坐标
+  dotDom.value!.style.left = x1 - 7 + 'Px';
+  dotDom.value!.style.top = y1 - 7 + 'Px';
+  // 绘制渐变色
+  const gradientDirection = {
+    x1: 0,
+    y1: 0,
+    x2: radius * 2, // 半径*2
+    y2: 0
+  };
+  gradientDirection.y2 = radius * 2;
+  const gradient = ctx.createLinearGradient(
+    gradientDirection.x1,
+    gradientDirection.y1,
+    gradientDirection.x2,
+    gradientDirection.y2
+  );
+  colorList.map((color, index) => {
+    gradient.addColorStop(index, color);
+  });
+  ctx.strokeStyle = gradient;
+  ctx.lineWidth = 10;
+  ctx.save();
+  ctx.beginPath();
+  ctx.arc(
+    canvasX,
+    canvasY,
+    radius,
+    -Math.PI / 2,
+    -Math.PI / 2 + steps * progress,
+    false
+  );
+  ctx.stroke();
+  ctx.closePath();
+  ctx.restore();
+}
+defineExpose({
+  startPlay,
+  pausePlay,
+  playState
+});
+</script>
+
+<style lang="less" scoped>
+.Metronome {
+  width: 656Px;
+  height: 554Px;
+  background: url('./imgs/bg.png') no-repeat;
+  background-size: 100% 100%;
+  padding: 26Px 26Px 0;
+  &:deep(.bom_drag) {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    height: 76Px;
+    padding: 0 28Px;
+    & > div {
+      width: 40Px;
+      height: 40Px;
+      border-radius: 0 0 18Px 0;
+      &:first-child {
+        border-radius: 0 0 0 18Px;
+      }
+    }
+  }
+  .MetronomeBox {
+    width: 100%;
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    z-index: 2;
+  }
+  .headTools {
+    position: absolute;
+    right: 20Px;
+    top: 18Px;
+    & > img {
+      margin-left: 18Px;
+      width: 24Px;
+      height: 24Px;
+      cursor: pointer;
+    }
+  }
+  .beatWrap {
+    margin-top: 30Px;
+    width: 252Px;
+    height: 252Px;
+    position: relative;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    .dot {
+      position: absolute;
+      width: 24Px;
+      height: 24Px;
+      background: url('./imgs/dot.png') no-repeat;
+      background-size: 100% 100%;
+      cursor: pointer;
+      z-index: 3;
+    }
+    .round {
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 100%;
+      height: 100%;
+      border-radius: 50%;
+      border: 10Px solid #d1d1d1;
+      z-index: 1;
+    }
+    .mycanvas {
+      position: absolute;
+      left: 0;
+      top: 0;
+      z-index: 2;
+    }
+    .beatCon {
+      border-radius: 50%;
+      z-index: 4;
+      width: 200Px;
+      height: 200Px;
+      background: url('./imgs/yuan.png') no-repeat;
+      background-size: 100% 100%;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      .optMid {
+        margin: 5Px 0;
+        display: flex;
+        align-items: center;
+        :deep(.n-input-number) {
+          width: 74Px;
+          height: 52Px;
+          margin-left: 4Px;
+          .n-input {
+            height: 100%;
+            --n-height: 100% !important;
+            --n-color-focus: initial !important;
+            --n-color: initial !important;
+            --n-padding-left: initial !important;
+            --n-padding-right: initial !important;
+            --n-color-disabled: initial !important;
+            --n-caret-color: #3a3939 !important;
+            .n-input__border,
+            .n-input__state-border {
+              display: none;
+            }
+            .n-input__input-el {
+              text-align: center;
+              font-family: DINAlternate, DINAlternate !important;
+              font-weight: bold !important;
+              font-size: 40Px !important;
+              color: #000000 !important;
+            }
+          }
+        }
+      }
+      .optImg {
+        cursor: pointer;
+        width: 35Px;
+        height: 35Px;
+      }
+    }
+  }
+  .selectCon {
+    display: flex;
+    margin-top: 22Px;
+    :deep(.n-select) {
+      width: 130Px;
+      .n-base-selection--selected {
+        --n-height: 48Px !important;
+        --n-border-radius: 24Px !important;
+        .n-base-selection__border,
+        .n-base-selection__state-border {
+          display: none;
+        }
+        .n-base-selection-input {
+          padding: 0;
+          .n-base-selection-input__content {
+            .beatName {
+              display: flex;
+              justify-content: center;
+              align-items: center;
+              & > div:first-child {
+                font-family: PingFangSC, PingFang SC;
+                font-weight: 500;
+                font-size: 30Px;
+                color: #333333;
+              }
+              & > div:last-child {
+                margin-left: 2Px;
+                font-family: PingFangSC, PingFang SC;
+                font-weight: 500;
+                font-size: 14Px;
+                color: #333333;
+              }
+            }
+          }
+        }
+      }
+    }
+    :deep(.beatSymbolSel.n-select) {
+      margin-left: 15Px;
+      .n-base-selection-input__content {
+        display: flex;
+        justify-content: center;
+        .beatSymbolImg {
+          width: 44Px;
+          height: 46Px;
+          &.beatSymbolImg1 {
+            background: url('./imgs/b1.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg2 {
+            background: url('./imgs/b2.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg3 {
+            background: url('./imgs/b3.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg4 {
+            background: url('./imgs/b4.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg5 {
+            background: url('./imgs/b5.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg6 {
+            background: url('./imgs/b6.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg7 {
+            background: url('./imgs/b7.png') no-repeat;
+            background-size: 100% 100%;
+          }
+        }
+      }
+    }
+  }
+  .sliderList {
+    margin-top: 24Px;
+    display: flex;
+    align-items: center;
+    & > img {
+      width: 18Px;
+      height: 18Px;
+      margin-right: 16Px;
+    }
+    .sliderText {
+      width: 32Px;
+      font-weight: 500;
+      font-size: 16Px;
+      color: #1cacf1;
+      margin-left: 16Px;
+    }
+    :deep(.n-slider-rail) {
+      height: 6Px;
+      line-height: 6Px;
+      background: #ffffff;
+      border-radius: 4Px;
+      width: 331Px;
+      .n-slider-rail__fill {
+        background: linear-gradient(90deg, #63daff 0%, #1798ff 100%);
+        border-radius: 4Px;
+      }
+    }
+    .thumbDot {
+      width: 21Px;
+      height: 21Px;
+    }
+  }
+  .playBtn {
+    cursor: pointer;
+    margin-top: 20Px;
+    width: 272Px;
+    height: 54Px;
+    background: #00acff;
+    border-radius: 27Px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-weight: 600;
+    font-size: 18Px;
+    color: #ffffff;
+    & > img {
+      margin-left: 14Px;
+      width: 13Px;
+      height: 14Px;
+      &.pauseImg {
+        width: 12Px;
+        height: 13Px;
+      }
+    }
+  }
+}
+</style>
+<style lang="less">
+.n-base-select-menu.n-select-menu {
+  .beatValOptItem {
+    padding: 0 9Px !important;
+    display: flex;
+    justify-content: center;
+    &.n-base-select-option--selected {
+      .n-base-select-option__content {
+        color: #47a7fe;
+      }
+    }
+    &.n-base-select-option--pending {
+      .n-base-select-option__content {
+        color: #ffffff;
+      }
+      &::before {
+        background: #47a7fe !important;
+        border-radius: 6Px !important;
+      }
+    }
+    .n-base-select-option__content {
+      font-weight: 500;
+      font-size: 18Px;
+      color: #777777;
+    }
+  }
+  .beatSymbolOptItem {
+    display: flex;
+    justify-content: center;
+    &.n-base-select-option--selected {
+      .n-base-select-option__content {
+        .beatSymbolImg {
+          &.beatSymbolImg1 {
+            background: url('./imgs/a1.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg2 {
+            background: url('./imgs/a2.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg3 {
+            background: url('./imgs/a3.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg4 {
+            background: url('./imgs/a4.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg5 {
+            background: url('./imgs/a5.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg6 {
+            background: url('./imgs/a6.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg7 {
+            background: url('./imgs/a7.png') no-repeat;
+            background-size: 100% 100%;
+          }
+        }
+      }
+    }
+    &.n-base-select-option--pending {
+      .n-base-select-option__content {
+        .beatSymbolImg {
+          &.beatSymbolImg1 {
+            background: url('./imgs/c1.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg2 {
+            background: url('./imgs/c2.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg3 {
+            background: url('./imgs/c3.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg4 {
+            background: url('./imgs/c4.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg5 {
+            background: url('./imgs/c5.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg6 {
+            background: url('./imgs/c6.png') no-repeat;
+            background-size: 100% 100%;
+          }
+          &.beatSymbolImg7 {
+            background: url('./imgs/c7.png') no-repeat;
+            background-size: 100% 100%;
+          }
+        }
+      }
+      &::before {
+        background: #47a7fe !important;
+        border-radius: 6Px !important;
+      }
+    }
+    .beatSymbolImg {
+      width: 44Px;
+      height: 46Px;
+      &.beatSymbolImg1 {
+        background: url('./imgs/b1.png') no-repeat;
+        background-size: 100% 100%;
+      }
+      &.beatSymbolImg2 {
+        background: url('./imgs/b2.png') no-repeat;
+        background-size: 100% 100%;
+      }
+      &.beatSymbolImg3 {
+        background: url('./imgs/b3.png') no-repeat;
+        background-size: 100% 100%;
+      }
+      &.beatSymbolImg4 {
+        background: url('./imgs/b4.png') no-repeat;
+        background-size: 100% 100%;
+      }
+      &.beatSymbolImg5 {
+        background: url('./imgs/b5.png') no-repeat;
+        background-size: 100% 100%;
+      }
+      &.beatSymbolImg6 {
+        background: url('./imgs/b6.png') no-repeat;
+        background-size: 100% 100%;
+      }
+      &.beatSymbolImg7 {
+        background: url('./imgs/b7.png') no-repeat;
+        background-size: 100% 100%;
+      }
+    }
+  }
+}
+</style>

+ 214 - 0
src/components/Metronome/MetronomeBox.vue

@@ -0,0 +1,214 @@
+<template>
+  <div
+    v-if="windowMet"
+    class="metronomeMinCon"
+    :class="[metronomeMinConBoxClass]"
+    :style="metronomeMinConDragData.styleDrag.value"
+  >
+    <div class="topMetronomeMin">
+      <img
+        class="zhen"
+        :style="{
+          '--rotateWagTime': rotateWagTime + 's'
+        }"
+        :class="{ pausedWagAnimation: playState === 'pause' }"
+        src="./imgs/zhen.png"
+      />
+      <img class="bai" src="./imgs/bai.png" />
+    </div>
+    <div class="bomMetronomeMin">
+      <img class="setting" @click="handleSetting" src="./imgs/setting.png" />
+      <img
+        class="play"
+        v-if="playState === 'pause'"
+        @click="
+          () => {
+            metronomeDom?.startPlay();
+            playState = 'play';
+          }
+        "
+        src="./imgs/paly.png"
+      />
+      <img
+        class="pause"
+        @click="
+          () => {
+            metronomeDom?.pausePlay();
+            playState = 'pause';
+          }
+        "
+        v-else
+        src="./imgs/pause.png"
+      />
+      <img class="close" @click="handleCloseMet" src="./imgs/close.png" />
+    </div>
+  </div>
+  <NModal
+    :style="dragStyle"
+    class="metronomeNModal"
+    v-model:show="metronomeShow"
+    :class="[dragClass, windowMet ? 'transformOrigin' : '']"
+    :display-directive="'show'"
+    @after-enter="
+      () => {
+        animationEnds = true;
+      }
+    "
+    @after-leave="
+      () => {
+        animationEnds = false;
+      }
+    "
+  >
+    <div>
+      <div class="topDragDom"></div>
+      <Metronome
+        v-if="windowMet || metronomeShow || animationEnds"
+        ref="metronomeDom"
+        @closeMet="handleCloseMet"
+        @windowMet="handleWindowMet"
+        @speedNumChange="handleSpeedNumChange"
+      ></Metronome>
+    </div>
+  </NModal>
+</template>
+
+<script setup lang="ts">
+import { NModal } from 'naive-ui';
+import { computed, ref, watch } from 'vue';
+import Metronome from './Metronome.vue';
+import { useUserStore } from '@/store/modules/users';
+import useDrag from '@/hooks/useDrag';
+
+const props = defineProps<{
+  modelValue: boolean;
+  dragClass: string;
+  dragStyle: Record<string, any>;
+}>();
+const emits = defineEmits<{
+  (e: 'update:modelValue', value: boolean): void;
+}>();
+const animationEnds = ref(true); //防止动画没结束 窗口就消失了
+const metronomeDom = ref<InstanceType<typeof Metronome>>();
+/* 窗口化 */
+const windowMet = ref(false);
+const metronomeShow = computed({
+  get() {
+    return props.modelValue;
+  },
+  set(value) {
+    emits('update:modelValue', value);
+  }
+});
+watch(metronomeShow, () => {
+  if (metronomeShow.value) {
+    windowMet.value = false;
+  }
+});
+const playState = ref<'play' | 'pause'>('pause');
+const rotateWagTime = ref((60 * 2) / 90);
+
+function handleCloseMet() {
+  metronomeShow.value = false;
+  windowMet.value = false;
+}
+function handleWindowMet() {
+  metronomeShow.value = false;
+  windowMet.value = true;
+  playState.value = metronomeDom.value!.playState;
+}
+function handleSetting() {
+  metronomeShow.value = true;
+}
+function handleSpeedNumChange(num: number) {
+  rotateWagTime.value = parseFloat(((60 * 2) / num).toFixed(4));
+}
+/* 指针拖动 */
+const users = useUserStore();
+const metronomeMinConBoxClass = 'metronomeMinConBoxClass_drag';
+const metronomeMinConDragData = useDrag(
+  [`${metronomeMinConBoxClass}`],
+  metronomeMinConBoxClass,
+  windowMet,
+  users.info.id
+);
+</script>
+
+<style lang="less" scoped>
+.metronomeMinCon {
+  position: fixed;
+  left: 12Px;
+  bottom: 10Px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  z-index: 10;
+  height: 81Px;
+  .topMetronomeMin {
+    position: relative;
+    display: flex;
+    pointer-events: none;
+    .bai {
+      width: 37Px;
+      height: 46Px;
+    }
+    .zhen {
+      width: 13Px;
+      height: 35Px;
+      position: absolute;
+      bottom: 6Px;
+      left: 50%;
+      transform: translateX(-50%);
+      transform-origin: 50% 100%;
+      animation: rotateWag var(--rotateWagTime) linear infinite;
+      &.pausedWagAnimation {
+        animation-play-state: paused;
+      }
+      @keyframes rotateWag {
+        0% {
+          transform: translateX(-50%) rotate(0deg);
+        }
+        25% {
+          transform: translateX(-50%) rotate(90deg);
+        }
+        50% {
+          transform: translateX(-50%) rotate(0deg);
+        }
+        75% {
+          transform: translateX(-50%) rotate(-90deg);
+        }
+        100% {
+          transform: translateX(-50%) rotate(0deg);
+        }
+      }
+    }
+  }
+  .bomMetronomeMin {
+    margin-top: 10Px;
+    display: flex;
+    & > img {
+      cursor: pointer;
+      width: 34Px;
+      height: 25Px;
+    }
+    .play,
+    .pause {
+      margin: 0 4Px;
+    }
+  }
+}
+.metronomeNModal {
+  position: relative;
+  box-shadow: initial;
+  &.transformOrigin {
+    transform-origin: 50% 50% !important;
+  }
+  .topDragDom {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 40Px;
+  }
+}
+</style>

BIN
src/components/Metronome/audio/tick.wav


BIN
src/components/Metronome/audio/tock.wav


BIN
src/components/Metronome/imgs/a1.png


BIN
src/components/Metronome/imgs/a2.png


BIN
src/components/Metronome/imgs/a3.png


BIN
src/components/Metronome/imgs/a4.png


BIN
src/components/Metronome/imgs/a5.png


BIN
src/components/Metronome/imgs/a6.png


BIN
src/components/Metronome/imgs/a7.png


BIN
src/components/Metronome/imgs/b1.png


BIN
src/components/Metronome/imgs/b2.png


BIN
src/components/Metronome/imgs/b3.png


BIN
src/components/Metronome/imgs/b4.png


BIN
src/components/Metronome/imgs/b5.png


BIN
src/components/Metronome/imgs/b6.png


BIN
src/components/Metronome/imgs/b7.png


BIN
src/components/Metronome/imgs/bai.png


BIN
src/components/Metronome/imgs/bg.png


BIN
src/components/Metronome/imgs/c1.png


BIN
src/components/Metronome/imgs/c2.png


BIN
src/components/Metronome/imgs/c3.png


BIN
src/components/Metronome/imgs/c4.png


BIN
src/components/Metronome/imgs/c5.png


BIN
src/components/Metronome/imgs/c6.png


BIN
src/components/Metronome/imgs/c7.png


BIN
src/components/Metronome/imgs/close.png


BIN
src/components/Metronome/imgs/closeMet.png


BIN
src/components/Metronome/imgs/dot.png


BIN
src/components/Metronome/imgs/dotBtn.png


BIN
src/components/Metronome/imgs/jia.png


BIN
src/components/Metronome/imgs/jian.png


BIN
src/components/Metronome/imgs/minMet.png


BIN
src/components/Metronome/imgs/paly.png


BIN
src/components/Metronome/imgs/pause.png


BIN
src/components/Metronome/imgs/pauseBtn.png


BIN
src/components/Metronome/imgs/playtBtn.png


BIN
src/components/Metronome/imgs/setting.png


BIN
src/components/Metronome/imgs/sound.png


BIN
src/components/Metronome/imgs/yin.png


BIN
src/components/Metronome/imgs/yuan.png


BIN
src/components/Metronome/imgs/zhen.png


+ 2 - 0
src/components/Metronome/index.ts

@@ -0,0 +1,2 @@
+import MetronomeBox from './MetronomeBox.vue';
+export default MetronomeBox;

+ 68 - 0
src/components/Metronome/useMetronome.ts

@@ -0,0 +1,68 @@
+import { ref, Ref, watch, onUnmounted } from 'vue';
+import tickWav from './audio/tick.wav';
+import tockWav from './audio/tock.wav';
+/*  播放相关 */
+export default function useMetronome(
+  beatVal: Ref<string>,
+  beatSymbol: Ref<string>
+) {
+  let _timeTask: NodeJS.Timer;
+  const playerTick = new Audio(tickWav);
+  const playerTock = new Audio(tockWav);
+  /* 音量 */
+  const volumeNum = ref(100);
+  watch(volumeNum, () => {
+    playerTick.volume = volumeNum.value / 100;
+    playerTock.volume = volumeNum.value / 100;
+  });
+  /* 播放状态 */
+  const playState = ref<'play' | 'pause'>('pause');
+  /* 速度 */
+  const speedNum = ref(90);
+
+  onUnmounted(() => {
+    pausePlay();
+  });
+  // 开始播放
+  function startPlay() {
+    playerTick.currentTime = 0;
+    playerTock.currentTime = 0;
+    const timeArr = computeTimeArr();
+    handleBeatPlay(timeArr, speedNum.value);
+    playState.value = 'play';
+  }
+  //暂停播放
+  function pausePlay() {
+    playerTick.pause();
+    playerTock.pause();
+    clearTimeout(_timeTask);
+    playState.value = 'pause';
+  }
+  function handleBeatPlay(timeArr: string[], speed: number) {
+    let index = 0;
+    timeTask();
+    function timeTask() {
+      const playVm = index === 0 ? playerTick : playerTock;
+      playVm.play();
+      playVm.onended = () => {
+        _timeTask = setTimeout(() => {
+          index === timeArr.length - 1 ? (index = 0) : index++;
+          timeTask();
+        }, ((1000 * 60) / speed) * Number(timeArr[index]));
+      };
+    }
+  }
+  function computeTimeArr() {
+    if (beatSymbol.value === '1') {
+      return beatVal.value.split('-');
+    }
+    return beatSymbol.value.split('-');
+  }
+  return {
+    volumeNum,
+    playState,
+    speedNum,
+    startPlay,
+    pausePlay
+  };
+}

+ 19 - 3
src/components/layout/index.tsx

@@ -27,6 +27,7 @@ import toneImage from './images/toneImage.png';
 import setTimeImage from './images/setTimeImage.png';
 import dragingBoxIcon from './images/dragingBoxIcon.png';
 import TimerMeter from '../timerMeter';
+import Metronome from '../Metronome';
 import { useRoute, useRouter } from 'vue-router';
 import { vaildUrl } from '/src/utils/urlUtils';
 import ChioseModal from '/src/views/home/modals/chioseModal';
@@ -613,7 +614,7 @@ export default defineComponent({
     /* 弹窗加拖动 */
     // 引导页
     getGuidanceShow();
-    // 选择课件弹窗
+    //计时器
     const users = useUserStore();
     const timerMeterConBoxClass = 'timerMeterConBoxClass_drag';
     const timerMeterConDragData = useDrag(
@@ -625,6 +626,17 @@ export default defineComponent({
       showModalTime,
       users.info.id
     );
+    // 节拍器
+    const metronomeConBoxClass = 'metronomeConBoxClass_drag';
+    const metronomeConBoxDragData = useDrag(
+      [
+        `${metronomeConBoxClass} .topDragDom`,
+        `${metronomeConBoxClass} .bom_drag`
+      ],
+      metronomeConBoxClass,
+      showModalBeat,
+      users.info.id
+    );
     return () => (
       <div class={[styles.wrap, 'wrap']}>
         <div>
@@ -778,7 +790,7 @@ export default defineComponent({
           />
         )}
 
-        <NModal
+        {/* <NModal
           class={['modalTitle background']}
           style={{ width: '687px' }}
           title={'节拍器'}
@@ -795,7 +807,11 @@ export default defineComponent({
               }}
               height={'650px'}></iframe>
           </div>
-        </NModal>
+        </NModal> */}
+        <Metronome
+          v-model={showModalBeat.value}
+          dragClass={metronomeConBoxClass}
+          dragStyle={metronomeConBoxDragData.styleDrag.value}></Metronome>
         <NModal v-model:show={showModalTone.value} class={['background']}>
           {/* <div
             onClick={() => {

+ 8 - 4
src/views/attend-class/index.tsx

@@ -51,6 +51,7 @@ import {
 } from '../prepare-lessons/api';
 import { vaildUrl } from '/src/utils/urlUtils';
 import TimerMeter from '/src/components/timerMeter';
+import Metronome from '/src/components/Metronome';
 import { iframeDislableKeyboard, px2vw } from '/src/utils';
 import PlaceholderTone from '/src/components/layout/modals/placeholderTone';
 import { state as globalState } from '/src/state';
@@ -1537,7 +1538,7 @@ export default defineComponent({
     const metronomeConBoxClass = 'metronomeConBoxClass_drag';
     const metronomeConBoxDragData = useDrag(
       [
-        `${metronomeConBoxClass}>.n-card-header`,
+        `${metronomeConBoxClass} .topDragDom`,
         `${metronomeConBoxClass} .bom_drag`
       ],
       metronomeConBoxClass,
@@ -2244,7 +2245,7 @@ export default defineComponent({
           <Dragbom></Dragbom>
         </NModal>
 
-        <NModal
+        {/* <NModal
           transformOrigin="center"
           class={['modalTitle background', metronomeConBoxClass]}
           title={'节拍器'}
@@ -2266,8 +2267,11 @@ export default defineComponent({
               height={'650px'}></iframe>
             <Dragbom></Dragbom>
           </div>
-        </NModal>
-
+        </NModal> */}
+        <Metronome
+          v-model={showModalBeat.value}
+          dragClass={metronomeConBoxClass}
+          dragStyle={metronomeConBoxDragData.styleDrag.value}></Metronome>
         <NModal
           transformOrigin="center"
           class={['background']}