lex 1 年之前
父节点
当前提交
35a6e7fc13
共有 87 个文件被更改,包括 1286 次插入0 次删除
  1. 8 0
      src/router/routes-common.ts
  2. 595 0
      src/views/tempo-practice/beat-desc.ts
  3. 二进制
      src/views/tempo-practice/images/bg.png
  4. 二进制
      src/views/tempo-practice/images/btn-1.png
  5. 二进制
      src/views/tempo-practice/images/btn-2.png
  6. 二进制
      src/views/tempo-practice/images/btn-3.png
  7. 二进制
      src/views/tempo-practice/images/btn-4.png
  8. 二进制
      src/views/tempo-practice/images/btn-5.png
  9. 二进制
      src/views/tempo-practice/images/icon-add.png
  10. 二进制
      src/views/tempo-practice/images/icon-arrow.png
  11. 二进制
      src/views/tempo-practice/images/icon-arrow2.png
  12. 二进制
      src/views/tempo-practice/images/icon-back.png
  13. 二进制
      src/views/tempo-practice/images/icon-close.png
  14. 二进制
      src/views/tempo-practice/images/icon-pause.png
  15. 二进制
      src/views/tempo-practice/images/icon-play.png
  16. 二进制
      src/views/tempo-practice/images/icon-plus.png
  17. 二进制
      src/views/tempo-practice/images/icon-set-title.png
  18. 二进制
      src/views/tempo-practice/images/icon-setting.png
  19. 二进制
      src/views/tempo-practice/images/icon-title.png
  20. 二进制
      src/views/tempo-practice/images/music/f-1.png
  21. 二进制
      src/views/tempo-practice/images/music/f-10.png
  22. 二进制
      src/views/tempo-practice/images/music/f-11.png
  23. 二进制
      src/views/tempo-practice/images/music/f-12.png
  24. 二进制
      src/views/tempo-practice/images/music/f-13.png
  25. 二进制
      src/views/tempo-practice/images/music/f-14.png
  26. 二进制
      src/views/tempo-practice/images/music/f-15.png
  27. 二进制
      src/views/tempo-practice/images/music/f-16.png
  28. 二进制
      src/views/tempo-practice/images/music/f-17.png
  29. 二进制
      src/views/tempo-practice/images/music/f-18.png
  30. 二进制
      src/views/tempo-practice/images/music/f-19.png
  31. 二进制
      src/views/tempo-practice/images/music/f-2.png
  32. 二进制
      src/views/tempo-practice/images/music/f-20.png
  33. 二进制
      src/views/tempo-practice/images/music/f-21.png
  34. 二进制
      src/views/tempo-practice/images/music/f-22.png
  35. 二进制
      src/views/tempo-practice/images/music/f-23.png
  36. 二进制
      src/views/tempo-practice/images/music/f-24.png
  37. 二进制
      src/views/tempo-practice/images/music/f-25.png
  38. 二进制
      src/views/tempo-practice/images/music/f-26.png
  39. 二进制
      src/views/tempo-practice/images/music/f-27.png
  40. 二进制
      src/views/tempo-practice/images/music/f-28.png
  41. 二进制
      src/views/tempo-practice/images/music/f-29.png
  42. 二进制
      src/views/tempo-practice/images/music/f-3.png
  43. 二进制
      src/views/tempo-practice/images/music/f-30.png
  44. 二进制
      src/views/tempo-practice/images/music/f-31.png
  45. 二进制
      src/views/tempo-practice/images/music/f-4.png
  46. 二进制
      src/views/tempo-practice/images/music/f-5.png
  47. 二进制
      src/views/tempo-practice/images/music/f-6.png
  48. 二进制
      src/views/tempo-practice/images/music/f-7.png
  49. 二进制
      src/views/tempo-practice/images/music/f-8.png
  50. 二进制
      src/views/tempo-practice/images/music/f-9.png
  51. 5 0
      src/views/tempo-practice/images/music/index.ts
  52. 二进制
      src/views/tempo-practice/images/music/j-1.png
  53. 二进制
      src/views/tempo-practice/images/music/j-10.png
  54. 二进制
      src/views/tempo-practice/images/music/j-11.png
  55. 二进制
      src/views/tempo-practice/images/music/j-12.png
  56. 二进制
      src/views/tempo-practice/images/music/j-13.png
  57. 二进制
      src/views/tempo-practice/images/music/j-14.png
  58. 二进制
      src/views/tempo-practice/images/music/j-15.png
  59. 二进制
      src/views/tempo-practice/images/music/j-16.png
  60. 二进制
      src/views/tempo-practice/images/music/j-17.png
  61. 二进制
      src/views/tempo-practice/images/music/j-18.png
  62. 二进制
      src/views/tempo-practice/images/music/j-19.png
  63. 二进制
      src/views/tempo-practice/images/music/j-2.png
  64. 二进制
      src/views/tempo-practice/images/music/j-20.png
  65. 二进制
      src/views/tempo-practice/images/music/j-21.png
  66. 二进制
      src/views/tempo-practice/images/music/j-22.png
  67. 二进制
      src/views/tempo-practice/images/music/j-23.png
  68. 二进制
      src/views/tempo-practice/images/music/j-24.png
  69. 二进制
      src/views/tempo-practice/images/music/j-25.png
  70. 二进制
      src/views/tempo-practice/images/music/j-26.png
  71. 二进制
      src/views/tempo-practice/images/music/j-27.png
  72. 二进制
      src/views/tempo-practice/images/music/j-28.png
  73. 二进制
      src/views/tempo-practice/images/music/j-29.png
  74. 二进制
      src/views/tempo-practice/images/music/j-3.png
  75. 二进制
      src/views/tempo-practice/images/music/j-30.png
  76. 二进制
      src/views/tempo-practice/images/music/j-31.png
  77. 二进制
      src/views/tempo-practice/images/music/j-4.png
  78. 二进制
      src/views/tempo-practice/images/music/j-5.png
  79. 二进制
      src/views/tempo-practice/images/music/j-6.png
  80. 二进制
      src/views/tempo-practice/images/music/j-7.png
  81. 二进制
      src/views/tempo-practice/images/music/j-8.png
  82. 二进制
      src/views/tempo-practice/images/music/j-9.png
  83. 217 0
      src/views/tempo-practice/index.module.less
  84. 129 0
      src/views/tempo-practice/index.tsx
  85. 110 0
      src/views/tempo-practice/setting-modal/index.module.less
  86. 128 0
      src/views/tempo-practice/setting-modal/index.tsx
  87. 94 0
      src/views/tempo-practice/setting.ts

+ 8 - 0
src/router/routes-common.ts

@@ -19,6 +19,14 @@ export default [
         } as metaType
       },
       {
+        path: '/tempo-practice',
+        name: 'tempo-practice',
+        component: () => import('@/views/tempo-practice'),
+        meta: {
+          title: '节奏练习'
+        }
+      },
+      {
         path: '/order-detail',
         name: 'order-detail',
         component: () => import('@/views/student-register/order-detail'),

+ 595 - 0
src/views/tempo-practice/beat-desc.ts

@@ -0,0 +1,595 @@
+export const beatDesc = {
+  1: {
+    beatNum: 1, // 元素数量
+    beatNo: '4-1', // 几分音符
+    attribute: [
+      {
+        number: 4, // 对应几分音符
+        type: 'sond' // 音符 | 休止符
+      }
+    ]
+  },
+  2: {
+    beatNum: 1,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 4,
+        type: 'rest'
+      }
+    ]
+  },
+  3: {
+    beatNum: 2,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  4: {
+    beatNum: 2,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 8,
+        type: 'reset'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  5: {
+    beatNum: 4,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      }
+    ]
+  },
+  6: {
+    beatNum: 3,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      }
+    ]
+  },
+  7: {
+    beatNum: 3,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  8: {
+    beatNum: 2,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 8,
+        point: true, // 点
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  9: {
+    beatNum: 2,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        point: true,
+        type: 'sond'
+      }
+    ]
+  },
+  10: {
+    beatNum: 3,
+    beatNo: '4-1',
+    liaison: true, // 是否连音
+    attribute: [
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  11: {
+    beatNum: 6,
+    beatNo: '4-1',
+    liaison: true,
+    attribute: [
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  12: {
+    beatNum: 3,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      }
+    ]
+  },
+  13: {
+    beatNum: 3,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 16,
+        type: 'reset'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      }
+    ]
+  },
+  14: {
+    beatNum: 4,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 16,
+        type: 'reset'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      }
+    ]
+  },
+
+  15: {
+    beatNum: 1,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 4,
+        point: true,
+        type: 'sond'
+      }
+    ]
+  },
+  16: {
+    beatNum: 1,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 4,
+        point: true,
+        type: 'reset'
+      }
+    ]
+  },
+  17: {
+    beatNum: 2,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 4,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  18: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 4,
+        type: 'sond'
+      }
+    ]
+  },
+  19: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  20: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'reset'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  21: {
+    beatNum: 4,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+
+  22: {
+    beatNum: 4,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      }
+    ]
+  },
+  23: {
+    beatNum: 4,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  24: {
+    beatNum: 4,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'reset'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      }
+    ]
+  },
+  25: {
+    beatNum: 5,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      }
+    ]
+  },
+  26: {
+    beatNum: 5,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  27: {
+    beatNum: 5,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 16,
+        type: 'reset'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  28: {
+    beatNum: 6,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      }
+    ]
+  },
+
+  29: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        point: true,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      }
+    ]
+  },
+  30: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        point: true,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      }
+    ]
+  },
+  31: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      },
+      {
+        number: 8,
+        type: 'sond'
+      },
+      {
+        number: 16,
+        type: 'sond'
+      }
+    ]
+  }
+};

二进制
src/views/tempo-practice/images/bg.png


二进制
src/views/tempo-practice/images/btn-1.png


二进制
src/views/tempo-practice/images/btn-2.png


二进制
src/views/tempo-practice/images/btn-3.png


二进制
src/views/tempo-practice/images/btn-4.png


二进制
src/views/tempo-practice/images/btn-5.png


二进制
src/views/tempo-practice/images/icon-add.png


二进制
src/views/tempo-practice/images/icon-arrow.png


二进制
src/views/tempo-practice/images/icon-arrow2.png


二进制
src/views/tempo-practice/images/icon-back.png


二进制
src/views/tempo-practice/images/icon-close.png


二进制
src/views/tempo-practice/images/icon-pause.png


二进制
src/views/tempo-practice/images/icon-play.png


二进制
src/views/tempo-practice/images/icon-plus.png


二进制
src/views/tempo-practice/images/icon-set-title.png


二进制
src/views/tempo-practice/images/icon-setting.png


二进制
src/views/tempo-practice/images/icon-title.png


二进制
src/views/tempo-practice/images/music/f-1.png


二进制
src/views/tempo-practice/images/music/f-10.png


二进制
src/views/tempo-practice/images/music/f-11.png


二进制
src/views/tempo-practice/images/music/f-12.png


二进制
src/views/tempo-practice/images/music/f-13.png


二进制
src/views/tempo-practice/images/music/f-14.png


二进制
src/views/tempo-practice/images/music/f-15.png


二进制
src/views/tempo-practice/images/music/f-16.png


二进制
src/views/tempo-practice/images/music/f-17.png


二进制
src/views/tempo-practice/images/music/f-18.png


二进制
src/views/tempo-practice/images/music/f-19.png


二进制
src/views/tempo-practice/images/music/f-2.png


二进制
src/views/tempo-practice/images/music/f-20.png


二进制
src/views/tempo-practice/images/music/f-21.png


二进制
src/views/tempo-practice/images/music/f-22.png


二进制
src/views/tempo-practice/images/music/f-23.png


二进制
src/views/tempo-practice/images/music/f-24.png


二进制
src/views/tempo-practice/images/music/f-25.png


二进制
src/views/tempo-practice/images/music/f-26.png


二进制
src/views/tempo-practice/images/music/f-27.png


二进制
src/views/tempo-practice/images/music/f-28.png


二进制
src/views/tempo-practice/images/music/f-29.png


二进制
src/views/tempo-practice/images/music/f-3.png


二进制
src/views/tempo-practice/images/music/f-30.png


二进制
src/views/tempo-practice/images/music/f-31.png


二进制
src/views/tempo-practice/images/music/f-4.png


二进制
src/views/tempo-practice/images/music/f-5.png


二进制
src/views/tempo-practice/images/music/f-6.png


二进制
src/views/tempo-practice/images/music/f-7.png


二进制
src/views/tempo-practice/images/music/f-8.png


二进制
src/views/tempo-practice/images/music/f-9.png


+ 5 - 0
src/views/tempo-practice/images/music/index.ts

@@ -0,0 +1,5 @@
+const modules: any = import.meta.glob('./*', { eager: true });
+export const getImage = (name: string) => {
+  // console.log(modules[`./${name}`]);
+  return modules[`./${name}`]?.default || '';
+};

二进制
src/views/tempo-practice/images/music/j-1.png


二进制
src/views/tempo-practice/images/music/j-10.png


二进制
src/views/tempo-practice/images/music/j-11.png


二进制
src/views/tempo-practice/images/music/j-12.png


二进制
src/views/tempo-practice/images/music/j-13.png


二进制
src/views/tempo-practice/images/music/j-14.png


二进制
src/views/tempo-practice/images/music/j-15.png


二进制
src/views/tempo-practice/images/music/j-16.png


二进制
src/views/tempo-practice/images/music/j-17.png


二进制
src/views/tempo-practice/images/music/j-18.png


二进制
src/views/tempo-practice/images/music/j-19.png


二进制
src/views/tempo-practice/images/music/j-2.png


二进制
src/views/tempo-practice/images/music/j-20.png


二进制
src/views/tempo-practice/images/music/j-21.png


二进制
src/views/tempo-practice/images/music/j-22.png


二进制
src/views/tempo-practice/images/music/j-23.png


二进制
src/views/tempo-practice/images/music/j-24.png


二进制
src/views/tempo-practice/images/music/j-25.png


二进制
src/views/tempo-practice/images/music/j-26.png


二进制
src/views/tempo-practice/images/music/j-27.png


二进制
src/views/tempo-practice/images/music/j-28.png


二进制
src/views/tempo-practice/images/music/j-29.png


二进制
src/views/tempo-practice/images/music/j-3.png


二进制
src/views/tempo-practice/images/music/j-30.png


二进制
src/views/tempo-practice/images/music/j-31.png


二进制
src/views/tempo-practice/images/music/j-4.png


二进制
src/views/tempo-practice/images/music/j-5.png


二进制
src/views/tempo-practice/images/music/j-6.png


二进制
src/views/tempo-practice/images/music/j-7.png


二进制
src/views/tempo-practice/images/music/j-8.png


二进制
src/views/tempo-practice/images/music/j-9.png


+ 217 - 0
src/views/tempo-practice/index.module.less

@@ -0,0 +1,217 @@
+.tempoPractice {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100vw;
+  height: 100vh;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  background: url("./images/bg.png") no-repeat center center / cover;
+
+  display: flex;
+  flex-direction: column;
+}
+
+.head {
+  position: relative;
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  padding: 0 23px 8px 41px;
+  transition: opacity 0.3s ease-in-out;
+
+  .back {
+    padding-top: 17px;
+
+    img {
+      width: 46px;
+      height: 46px;
+      display: block;
+    }
+  }
+
+  .title {
+    img {
+      width: 173px;
+      height: 75px;
+      display: block;
+    }
+  }
+}
+
+.container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex: 1 auto;
+  flex-wrap: wrap;
+  gap: 22px 0;
+}
+
+.beatSection {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  &.small {
+    margin: 0 16px;
+
+    .beat {
+      border: 2px solid #fff;
+      width: 70px;
+      height: 94px;
+      cursor: pointer;
+
+      &::before,
+      &::after {
+        width: 19px;
+        height: 5px;
+      }
+
+      img {
+        width: 48px;
+      }
+    }
+  }
+
+  &.minLength {
+    &:nth-child(2n + 1) {
+      margin-left: 10vw;
+    }
+
+    &:nth-child(2n + 2) {
+      margin-right: 10vw;
+    }
+  }
+
+  &.maxLength {
+    margin: 0 12px;
+
+    .beat {
+      margin: 0 7px;
+    }
+  }
+
+  .beat {
+    display: flex;
+    align-items: center;
+    flex-direction: column;
+    margin: 0 13px;
+    width: 118px;
+    height: 156px;
+    box-shadow: 0px 2px 16px 0px #76C3D2;
+    border-radius: 14px;
+    border: 3px solid #fff;
+    background: #FFFFFF;
+
+    &.active {
+      border: 3px solid rgba(255, 167, 0, 1);
+    }
+
+    .imgSection {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex: 1;
+    }
+
+    img {
+      width: 96px;
+    }
+
+    &::before,
+    &::after {
+      content: '';
+      display: block;
+      width: 30px;
+      height: 7px;
+      background: url('./images/icon-arrow.png') no-repeat center center / contain;
+      margin: 0 auto;
+    }
+
+    &::before {
+      margin-top: 3px;
+    }
+
+    &::after {
+      margin-bottom: 3px;
+      transform: rotate(180deg);
+    }
+  }
+}
+
+.footer {
+  padding: 12px 0 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  &>div {
+    margin: 0 9px;
+    cursor: pointer;
+
+    img {
+      width: inherit;
+      height: inherit;
+      display: block;
+    }
+  }
+
+  .play {
+    width: 54px;
+    height: 55px;
+  }
+
+  .playType {
+    width: 175px;
+    height: 39px;
+  }
+
+  .randomTempo {
+    width: 90px;
+    height: 39px;
+  }
+
+  .speedChange {
+    width: 110px;
+    height: 39px;
+    background: url('./images/btn-4.png') no-repeat center center / contain;
+    display: flex;
+    align-items: center;
+    padding: 9px;
+    display: flex;
+    align-items: center;
+
+
+    .speedNum {
+      flex: 1;
+      font-size: 16px;
+      font-weight: 600;
+      color: #6B3B19;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      &::after {
+        content: '';
+        display: inline-block;
+        width: 8px;
+        height: 5px;
+        margin-left: 4px;
+        background: url('./images/icon-arrow2.png') no-repeat center center / contain;
+      }
+    }
+  }
+
+  .speedPlus,
+  .speedAdd {
+    width: 21px;
+    height: 21px;
+  }
+}
+
+.settingPopup {
+  background: transparent;
+  overflow: visible;
+}

+ 129 - 0
src/views/tempo-practice/index.tsx

@@ -0,0 +1,129 @@
+import { defineComponent, onMounted, reactive } from 'vue';
+import styles from './index.module.less';
+import { postMessage } from '@/helpers/native-message';
+import icon_title from './images/icon-title.png';
+import icon_back from './images/icon-back.png';
+import icon_setting from './images/icon-setting.png';
+import iconPlay from './images/icon-play.png';
+import iconPause from './images/icon-pause.png';
+import beat from './images/btn-2.png';
+import tempo from './images/btn-3.png';
+import randDom from './images/btn-1.png';
+import iconPlus from './images/icon-plus.png';
+import iconAdd from './images/icon-add.png';
+import { getImage } from './images/music';
+import j1 from './images/music/j-1.png';
+// import j2 from './images/music/j-2.png';
+import { Popup } from 'vant';
+import SettingModal from './setting-modal';
+import { randomScoreElement, renderScore, setting } from './setting';
+
+export default defineComponent({
+  name: 'tempo-practice',
+  setup() {
+    const state = reactive({
+      settingStatus: false,
+      playState: 'pause' as 'pause' | 'play',
+      playType: 'beat' as 'beat' | 'tempo'
+    });
+    // 返回
+    const goback = () => {
+      postMessage({ api: 'goBack' });
+    };
+
+    /** 播放切换 */
+    const handlePlay = () => {
+      if (state.playState === 'pause') {
+        state.playState = 'play';
+      } else {
+        state.playState = 'pause';
+      }
+    };
+    /** 播放类型 */
+    const handlePlayType = () => {
+      if (state.playType === 'beat') {
+        state.playType = 'tempo';
+      } else {
+        state.playType = 'beat';
+      }
+    };
+
+    onMounted(() => {
+      renderScore();
+    });
+    return () => (
+      <div class={styles.tempoPractice}>
+        <div class={styles.head}>
+          <div class={styles.back} onClick={goback}>
+            <img src={icon_back} />
+          </div>
+          <div class={styles.title}>
+            <img src={icon_title} />
+          </div>
+          <div class={styles.back} onClick={() => (state.settingStatus = true)}>
+            <img src={icon_setting} />
+          </div>
+        </div>
+
+        <div class={styles.container}>
+          {setting.scorePart.map((item: any) => (
+            <div
+              class={[
+                styles.beatSection,
+                styles.small,
+                // styles.minLength,
+                styles.maxLength
+              ]}>
+              {item.map((child: any) => (
+                // , styles.active
+                <div
+                  class={[styles.beat]}
+                  onClick={() => {
+                    const obj = randomScoreElement(child.index);
+                    child.index = obj.index;
+                    child.url = obj.url;
+                  }}>
+                  <div class={styles.imgSection}>
+                    <img src={getImage(child.url)} />
+                  </div>
+                </div>
+              ))}
+            </div>
+          ))}
+        </div>
+        <div class={styles.footer}>
+          {/* 播放 */}
+          <div class={styles.play} onClick={handlePlay}>
+            {state.playState === 'pause' ? (
+              <img src={iconPause} />
+            ) : (
+              <img src={iconPlay} />
+            )}
+          </div>
+          {/* 播放类型 */}
+          <div class={styles.playType} onClick={handlePlayType}>
+            {state.playType === 'beat' ? (
+              <img src={beat} />
+            ) : (
+              <img src={tempo} />
+            )}
+          </div>
+          {/* 随机生成 */}
+          <div class={styles.randomTempo}>
+            <img src={randDom} />
+          </div>
+          {/* 速度 */}
+          <div class={styles.speedChange}>
+            <img src={iconPlus} class={styles.speedPlus} />
+            <div class={styles.speedNum}>90</div>
+            <img src={iconAdd} class={styles.speedAdd} />
+          </div>
+        </div>
+
+        <Popup v-model:show={state.settingStatus} class={styles.settingPopup}>
+          <SettingModal onClose={() => (state.settingStatus = false)} />
+        </Popup>
+      </div>
+    );
+  }
+});

+ 110 - 0
src/views/tempo-practice/setting-modal/index.module.less

@@ -0,0 +1,110 @@
+.settingContainer {
+  position: relative;
+  width: 430px;
+  height: 86vh;
+  background: #fff;
+  border-radius: 26px;
+  padding: 20px 0;
+
+  .title {
+    position: absolute;
+    left: 50%;
+    top: -6px;
+    margin-left: -70px;
+    width: 140px;
+    height: 34px;
+    background: url('../images/icon-set-title.png') no-repeat center center / contain;
+  }
+
+  .iconClose {
+    position: absolute;
+    right: 13px;
+    top: 13px;
+    z-index: 9;
+    display: inline-block;
+    width: 31px;
+    height: 32px;
+    background: url('../images/icon-close.png') no-repeat center center / contain;
+  }
+}
+
+.settingContent {
+  padding: 0 26px;
+  overflow-y: auto;
+  height: 100%;
+}
+
+.settingParams {
+  // padding: 20px 26px;
+  padding-bottom: 53px;
+}
+
+.parmaTitle {
+  font-size: 14px;
+  font-weight: 600;
+  color: #131415;
+  line-height: 20px;
+  padding-bottom: 8px;
+}
+
+.paramContent {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 0 12px;
+  padding-bottom: 18px;
+
+
+  .btn {
+    width: 64px;
+    height: 26px;
+    font-size: 12px;
+    font-weight: 600;
+    color: rgba(0, 0, 0, 0.7);
+    line-height: 17px;
+    background: #F5F6F7;
+    border: none;
+    padding: 0;
+
+    &.active {
+      background: #19AEFF;
+      color: #FFFFFF;
+    }
+  }
+
+  &.tempo {
+    gap: 8px 8px;
+    padding-bottom: 0;
+  }
+
+  .active {
+    background: #D0EBFF;
+  }
+
+  img {
+    width: 46px;
+    height: 46px;
+    background: #F5F6F7;
+    border-radius: 4px;
+  }
+}
+
+.btnGroup {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 14px 0;
+
+  .btnSubmit {
+    width: 143px;
+    height: 45px;
+    line-height: 45px;
+    border-radius: 20px;
+    background: url('../images/btn-5.png') no-repeat center center / contain;
+    border: none;
+  }
+}

+ 128 - 0
src/views/tempo-practice/setting-modal/index.tsx

@@ -0,0 +1,128 @@
+import { computed, defineComponent, onMounted, reactive } from 'vue';
+import styles from './index.module.less';
+import { Button } from 'vant';
+import {
+  barLineList,
+  beatList,
+  elementList,
+  setting,
+  tempo4,
+  tempo8
+} from '../setting';
+import { getImage } from '../images/music';
+
+export default defineComponent({
+  emits: ['close'],
+  name: 'setting-modal',
+  setup(props, { emit }) {
+    const state = reactive({
+      element: 'jianpu' as 'jianpu' | 'staff', // 元素
+      beat: '4-4' as '4-2' | '4-3' | '4-4' | '8-3' | '8-6', // 拍号
+      barLine: '1' as '1' | '2' | '4', // 小节数
+      tempo: ['1', '2', '3'] as any[] // 节奏形筛选
+    });
+
+    const tempoList = computed(() => {
+      if (['4-2', '4-3', '4-4'].includes(state.beat)) {
+        return tempo4;
+      } else if (['8-3', '8-6'].includes(state.beat)) {
+        return tempo8;
+      }
+      return tempo4;
+    });
+
+    const onChangeTempo = (item: any) => {
+      const index = state.tempo.indexOf(item);
+      if (index !== -1) {
+        state.tempo.splice(index, 1);
+      } else {
+        state.tempo.push(item);
+      }
+    };
+
+    const onSubmit = () => {
+      setting.element = state.element;
+      setting.beat = state.beat;
+      setting.barLine = state.barLine;
+      setting.tempo = state.tempo;
+
+      emit('close');
+    };
+
+    onMounted(() => {});
+    return () => (
+      <div class={styles.settingContainer}>
+        <div class={styles.title}></div>
+        <i class={styles.iconClose} onClick={() => emit('close')}></i>
+
+        <div class={styles.settingContent}>
+          <div class={styles.settingParams}>
+            <div class={styles.parmaTitle}>元素</div>
+            <div class={styles.paramContent}>
+              {Object.keys(elementList).map((item: any) => (
+                <Button
+                  round
+                  class={[styles.btn, state.element === item && styles.active]}
+                  onClick={() => {
+                    state.element = item;
+                  }}>
+                  {elementList[item]}
+                </Button>
+              ))}
+            </div>
+            <div class={styles.parmaTitle}>拍号</div>
+            <div class={styles.paramContent}>
+              {Object.keys(beatList).map((item: any) => (
+                <Button
+                  round
+                  class={[styles.btn, state.beat === item && styles.active]}
+                  onClick={() => {
+                    state.beat = item;
+                    if (['4-2', '4-3', '4-4'].includes(state.beat)) {
+                      state.tempo = ['1', '2', '3'];
+                    } else if (['8-3', '8-6'].includes(state.beat)) {
+                      state.tempo = ['15', '16', '17'];
+                    }
+                  }}>
+                  {beatList[item]}
+                </Button>
+              ))}
+            </div>
+            <div class={styles.parmaTitle}>每页显示小节数量</div>
+            <div class={styles.paramContent}>
+              {Object.keys(barLineList).map((item: any) => (
+                <Button
+                  round
+                  class={[styles.btn, state.barLine === item && styles.active]}
+                  onClick={() => {
+                    state.barLine = item;
+                  }}>
+                  {barLineList[item]}
+                </Button>
+              ))}
+            </div>
+            <div class={styles.parmaTitle}>节奏型筛选</div>
+            <div class={[styles.paramContent, styles.tempo]}>
+              {Object.keys(tempoList.value).map((item: any) => (
+                <>
+                  <img
+                    onClick={() => onChangeTempo(item)}
+                    class={state.tempo.includes(item) && styles.active}
+                    src={getImage(
+                      (state.element === 'jianpu' ? 'j-' : 'f-') +
+                        tempoList.value[item]
+                    )}
+                  />
+                </>
+              ))}
+            </div>
+          </div>
+
+          <div class={styles.btnGroup}>
+            <Button class={styles.btnSubmit} onClick={onSubmit}></Button>
+          </div>
+        </div>
+      </div>
+    );
+  }
+});

+ 94 - 0
src/views/tempo-practice/setting.ts

@@ -0,0 +1,94 @@
+import { reactive } from 'vue';
+
+export const setting = reactive({
+  element: 'jianpu' as 'jianpu' | 'staff', // 元素
+  beat: '4-4' as '4-2' | '4-3' | '4-4' | '8-3' | '8-6', // 拍号
+  barLine: '1' as '1' | '2' | '4', // 小节数
+  tempo: ['1', '2', '3'] as any[], // 节奏形筛选
+  scorePart: [] as any // 生成谱面
+});
+
+/** 元素 */
+export const elementList = {
+  jianpu: '简谱',
+  staff: '五线谱'
+} as any;
+
+/** 拍号 */
+export const beatList = {
+  '4-2': '2/4',
+  '4-3': '3/4',
+  '4-4': '4/4',
+  '8-3': '3/8',
+  '8-6': '6/8'
+} as any;
+
+/** 每页小节数量 */
+export const barLineList = {
+  1: 1,
+  2: 2,
+  4: 4
+} as any;
+
+/** 节奏型筛选 */
+// 简谱
+const temp: any = {};
+for (let i = 1; i <= 14; i++) {
+  temp[i] = i + '.png';
+}
+export const tempo4 = temp;
+
+// 五线谱
+const temp2: any = {};
+for (let i = 15; i <= 31; i++) {
+  temp2[i] = i + '.png';
+}
+export const tempo8 = temp2;
+
+/** 随机生成元素 */
+export const randomScoreElement = (element?: string) => {
+  const tempoList = setting.tempo;
+  const prefix = setting.element === 'jianpu' ? 'j-' : 'f-';
+  if (element) {
+    const newArr = tempoList.filter((item: any) => item !== element);
+    // 生成一个0到newArr长度之间的随机索引
+    const randomIndex = Math.floor(Math.random() * newArr.length);
+    return {
+      url: prefix + newArr[randomIndex] + '.png',
+      index: newArr[randomIndex]
+    };
+  } else {
+    // 如果只有一个就直接返回
+    if (tempoList.length === 1) {
+      return {
+        url: prefix + tempoList[0] + '.png',
+        index: tempoList[0]
+      };
+    } else {
+      const randomIndex = Math.floor(Math.random() * tempoList.length);
+      const randomItem = tempoList[randomIndex];
+      return {
+        url: prefix + randomItem + '.png',
+        index: randomItem
+      };
+    }
+  }
+};
+
+/** 生成谱面 */
+export const renderScore = () => {
+  const barLine = Number(setting.barLine);
+  const beat = setting.beat.split('-')[1];
+  const tempBeat: any = [];
+  for (let i = 0; i < barLine; i++) {
+    tempBeat[i] = [];
+    for (let j = 0; j < Number(beat); j++) {
+      tempBeat[i][j] = {
+        ...randomScoreElement()
+      };
+    }
+  }
+
+  setting.scorePart = tempBeat;
+  console.log(tempBeat, 'tempBeat');
+};