Kaynağa Gözat

Merge branch 'iteration-20240612' into jenkins-test

lex 1 yıl önce
ebeveyn
işleme
3e2a42111c

+ 11 - 0
package-lock.json

@@ -20,6 +20,7 @@
         "eventemitter3": "^5.0.1",
         "howler": "^2.2.3",
         "html2canvas": "^1.4.1",
+        "mobile-drag-drop": "^3.0.0-rc.0",
         "moveable": "^0.52.0",
         "naive-ui": "^2.34.4",
         "numeral": "^2.0.6",
@@ -6500,6 +6501,11 @@
         "node": ">=10"
       }
     },
+    "node_modules/mobile-drag-drop": {
+      "version": "3.0.0-rc.0",
+      "resolved": "https://registry.npmmirror.com/mobile-drag-drop/-/mobile-drag-drop-3.0.0-rc.0.tgz",
+      "integrity": "sha512-f8wIDTbBYLBW/+5sei1cqUE+StyDpf/LP+FRZELlVX6tmOOmELk84r3wh1z3woxCB9G5octhF06K5COvFjGgqg=="
+    },
     "node_modules/moveable": {
       "version": "0.52.0",
       "resolved": "https://registry.npmmirror.com/moveable/-/moveable-0.52.0.tgz",
@@ -14247,6 +14253,11 @@
       "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
       "dev": true
     },
+    "mobile-drag-drop": {
+      "version": "3.0.0-rc.0",
+      "resolved": "https://registry.npmmirror.com/mobile-drag-drop/-/mobile-drag-drop-3.0.0-rc.0.tgz",
+      "integrity": "sha512-f8wIDTbBYLBW/+5sei1cqUE+StyDpf/LP+FRZELlVX6tmOOmELk84r3wh1z3woxCB9G5octhF06K5COvFjGgqg=="
+    },
     "moveable": {
       "version": "0.52.0",
       "resolved": "https://registry.npmmirror.com/moveable/-/moveable-0.52.0.tgz",

+ 1 - 0
package.json

@@ -34,6 +34,7 @@
     "eventemitter3": "^5.0.1",
     "howler": "^2.2.3",
     "html2canvas": "^1.4.1",
+    "mobile-drag-drop": "^3.0.0-rc.0",
     "moveable": "^0.52.0",
     "naive-ui": "^2.34.4",
     "numeral": "^2.0.6",

+ 2 - 2
src/helpers/deep-clone.ts

@@ -3,8 +3,8 @@ const deepClone = (obj: any) => {
   const clone = Object.assign({}, obj);
   Object.keys(clone).forEach(
     key =>
-      (clone[key] =
-        typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
+    (clone[key] =
+      typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
   );
   if (Array.isArray(obj)) {
     clone.length = obj.length;

+ 8 - 8
src/hooks/useDrag/index.ts

@@ -55,14 +55,14 @@ export default function useDrag(
     return pos.value.left === -1 && pos.value.top === -1
       ? {}
       : {
-          position: 'fixed',
-          left: `${pos.value.left}px`,
-          top: `${pos.value.top}px`,
-          transform: 'initial',
-          transformOrigin: 'initial',
-          margin: 'initial',
-          transition: 'initial'
-        };
+        position: 'fixed',
+        left: `${pos.value.left}px`,
+        top: `${pos.value.top}px`,
+        transform: 'initial',
+        transformOrigin: 'initial',
+        margin: 'initial',
+        transition: 'initial'
+      };
   });
   function initPos() {
     const posCache = getCachePos(useIdDargClass);

+ 16 - 0
src/main.ts

@@ -16,6 +16,22 @@ import { Lazyload, setToastDefaultOptions } from 'vant';
 import useErrorLog from './hooks/useErrorLog';
 setToastDefaultOptions({ duration: 3000 });
 
+import 'mobile-drag-drop/default.css';
+import { polyfill } from 'mobile-drag-drop';
+
+// 可选引入滚动行为的脚本
+import { scrollBehaviourDragImageTranslateOverride } from 'mobile-drag-drop/scroll-behaviour';
+
+// 可选的配置项
+const options = {
+  // 使用这个选项以应用滚动行为
+  dragImageTranslateOverride: scrollBehaviourDragImageTranslateOverride
+};
+
+// 初始化polyfill
+polyfill(options);
+
+
 // 获取token
 promisefiyPostMessage({ api: 'getToken' }).then((res: any) => {
   const content = res.content;

+ 21 - 0
src/styles/index.less

@@ -10,6 +10,27 @@
   --van-dialog-message-font-size: 16px;
 }
 
+img {
+  /* -webkit-touch-callout: none; */
+  -moz-user-select: none;
+  /* 火狐浏览器 */
+  -webkit-user-drag: none;
+  /* 谷歌、Safari和Opera浏览器 */
+  -webkit-user-select: none;
+  /* 谷歌、Safari和Opera浏览器 */
+  -ms-user-select: none;
+  /* IE10+浏览器 */
+  user-select: none;
+  /* 通用 */
+  -webkit-touch-callout: none;
+}
+
+body {
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+}
+
 // 默认输入框光标颜色
 input,
 textarea {

+ 44 - 14
src/views/tempo-practice/beat-tick.ts

@@ -1,7 +1,7 @@
-import { reactive } from 'vue';
+import { reactive, toRef, toRefs } from 'vue';
 import tockAndTick from './tockAndTick.json';
 import { Howl } from 'howler';
-import { initSelectScorePart, setting } from './setting';
+import { initSelectScorePart, initSelectScorePartModal, setting, setting_modal } from './setting';
 import { beatDesc } from './beat-desc';
 
 const beatData = reactive({
@@ -20,6 +20,8 @@ const beatData = reactive({
   show: false
 });
 
+let defaultSetting = {} as any
+
 const handlePlay = (i: number, source: any, timer: any) => {
   let payBeatTime = new Date().getTime();
   return new Promise(resolve => {
@@ -30,10 +32,14 @@ const handlePlay = (i: number, source: any, timer: any) => {
 
     let timeSppedEnum = 16.7;
     const proofTime = () => {
-      if (setting.playState !== 'play') {
+      if (defaultSetting.playState !== 'play') {
         return;
       }
       setTimeout(() => {
+        if (beatData.tickEnd) {
+          resolve(i);
+          return;
+        }
         const currentTime = new Date().getTime();
         // 两次定时任务时间间隔
         const diffTime = currentTime - payBeatTime;
@@ -70,19 +76,28 @@ const handlePlay = (i: number, source: any, timer: any) => {
   });
 };
 
-/** 开始节拍器 */
-export const handleStartBeat = async () => {
+/** 开始节拍器
+ * @param {boolean} 是否开启了设置
+ */
+export const handleStartBeat = async (settingStatus = false) => {
+  const tempSetting = settingStatus ? setting_modal : setting
+  defaultSetting = {
+    playState: tempSetting.playState,
+    speed: tempSetting.speed,
+    scorePart: tempSetting.scorePart
+  }
+
   beatData.show = true;
   beatData.tickEnd = false;
   beatData.index = 0;
-  beatData.beatLengthInMilliseconds = (60 / setting.speed) * 1000;
+  beatData.beatLengthInMilliseconds = (60 / defaultSetting.speed) * 1000;
   // let startTime = +new Date();
-  for (let i = 0; i < setting.scorePart.length; i++) {
+  for (let i = 0; i < defaultSetting.scorePart.length; i++) {
     if (beatData.tickEnd) return false;
-    for (let j = 0; j < setting.scorePart[i].length; j++) {
+    for (let j = 0; j < defaultSetting.scorePart[i].length; j++) {
       if (beatData.tickEnd) return false;
       // 提前结束, 直接放回false
-      const part = setting.scorePart[i][j];
+      const part = defaultSetting.scorePart[i][j];
       const params = {
         ...part,
         ...beatDesc[part.index]
@@ -127,14 +142,23 @@ export const handleStartBeat = async () => {
           type: attr.type
         });
         beatData.loopTime = time;
-        initSelectScorePart(i, j);
+        if (settingStatus) {
+          initSelectScorePartModal(i, j)
+        } else {
+          initSelectScorePart(i, j);
+        }
+
+        defaultSetting.playState = settingStatus ? setting_modal.playState : setting.playState
+        if (defaultSetting.playState !== 'play') {
+          hendleEndBeat(settingStatus)
+        }
       }
     }
   }
 
   // console.log(+new Date() - startTime);
   beatData.show = false;
-  handleStartBeat();
+  handleStartBeat(settingStatus);
   return true;
 };
 
@@ -152,8 +176,14 @@ export const handleInitBeat = (
   beatData.len = beat;
 };
 
-/** 节拍器暂停 */
-export const hendleEndBeat = () => {
+/** 节拍器暂停
+ * @param {boolean} 是否开启了设置
+ */
+export const hendleEndBeat = (settingStatus = false) => {
   beatData.tickEnd = true;
-  initSelectScorePart();
+  if (settingStatus) {
+    initSelectScorePartModal()
+  } else {
+    initSelectScorePart();
+  }
 };

BIN
src/views/tempo-practice/images/pc_bg.png


BIN
src/views/tempo-practice/images/setting-arrow-active.png


BIN
src/views/tempo-practice/images/setting-arrow-default.png


+ 98 - 34
src/views/tempo-practice/index.module.less

@@ -6,15 +6,18 @@
   height: 100vh;
   overflow: hidden;
   display: flex;
-  flex-direction: column;
+  // flex-direction: column;
   background: url("./images/bg.png") no-repeat center center / cover;
 
-  display: flex;
+  .containerLeft {
+    height: 100%;
+  }
 
   &.modal {
-    .head{
+    .head {
       padding: 0 23px 8px 23px;
     }
+
     .iconBack {
       opacity: 0;
       pointer-events: none;
@@ -31,28 +34,6 @@
       justify-content: center;
     }
   }
-
-  // &.dpi3 {
-  //   .beatSection {
-
-  //     .beat {
-  //       width: 102px;
-  //       height: 136px;
-  //     }
-
-  //     &.small {
-  //       width: 50%;
-
-  //       // margin: 0 16px;
-  //       &.dpi3 {
-  //         .beat {
-  //           width: 60px;
-  //           height: 72px;
-  //         }
-  //       }
-  //     }
-  //   }
-  // }
 }
 
 .conCon {
@@ -62,7 +43,6 @@
 }
 
 .pc {
-
   .container {
     max-width: 1200px;
     gap: 30px 0;
@@ -78,16 +58,11 @@
       }
     }
 
-
-
     &.small {
       .beat {
         width: 139px !important;
         height: 191px !important;
 
-        // width: 65px;
-        //   height: 86px;
-
         img {
           width: 108px;
         }
@@ -164,6 +139,7 @@
 
 
     .beat {
+      transition: all .1s ease;
       border: 2px solid #fff;
       // width: 65px;
       // height: 86px;
@@ -192,10 +168,11 @@
     width: 118px;
     height: 156px;
     box-shadow: 0px 2px 16px 0px #76C3D2;
-    border-radius: 14px;
+    border-radius: 5px;
     border: 3px solid #fff;
     background: #FFFFFF;
     position: relative;
+    transition: all .1s ease;
 
     .direction {
       position: absolute;
@@ -229,6 +206,14 @@
       width: 96px;
     }
 
+    &.disabledChange {
+
+      &::before,
+      &::after {
+        display: none;
+      }
+    }
+
     &::before,
     &::after {
       content: '';
@@ -374,9 +359,88 @@
     }
   }
 }
-:global{
-  .settingBoxClass_drag .settingContainer_pc{
+
+:global {
+  .settingBoxClass_drag .settingContainer_pc {
     border-radius: 16px;
     height: 50vh !important;
   }
 }
+
+
+.containerLeft {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  transition: all .2s ease;
+
+
+  &.leftShow {
+    .beatSection {
+      .beat {
+        width: 85px;
+        height: 105px;
+        transition: all .1s ease;
+
+        img {
+          width: 64px;
+        }
+      }
+
+      &.small {
+        &:nth-child(2n + 1) {
+          justify-content: flex-end;
+          padding-right: 4px;
+        }
+
+        &:nth-child(2n + 2) {
+          justify-content: flex-start;
+          padding-left: 4px;
+        }
+
+        .beat {
+          width: 42px !important;
+          height: 56px !important;
+          transition: all .1s ease;
+
+          img {
+            width: 28px;
+          }
+        }
+      }
+    }
+
+    .footer {
+      &>div {
+        margin: 0 7px;
+      }
+
+      .play {
+        width: 42px;
+        height: 42px;
+      }
+
+      .playType {
+        width: 175px;
+        height: 39px;
+      }
+    }
+  }
+
+}
+
+.containerRight {
+  width: 281px;
+  transition: all .2s ease;
+
+  &.rightHide {
+    transform: translateX(100%);
+    width: 0;
+    transition: all .2s ease;
+  }
+}
+
+.settingModalShow {
+  height: 100% !important;
+  width: 281px !important;
+}

+ 341 - 220
src/views/tempo-practice/index.tsx

@@ -30,7 +30,10 @@ import {
   randomScoreElement,
   renderScore,
   setting,
-  elementDirection
+  elementDirection,
+  setting_modal,
+  initSelectScorePartModal,
+  renderScoreModal
 } from './setting';
 import { handleStartTick, hendleEndTick } from './tick';
 import { handleStartBeat, hendleEndBeat } from './beat-tick';
@@ -39,6 +42,7 @@ import { useRoute } from 'vue-router';
 import useDrag from '@/hooks/useDrag';
 import useDragGuidance from '@/hooks/useDrag/useDragGuidance';
 import { state as stateData } from '@/state';
+import SettingPcModal from './setting-pc-modal';
 
 export default defineComponent({
   name: 'tempo-practice',
@@ -63,6 +67,7 @@ export default defineComponent({
       platform: route.query.platform,
       win: route.query.win,
       settingStatus: false,
+      settingPcStatus: false,
       speedList: [
         { text: '40', value: 40, color: '#060606' },
         { text: '50', value: 50, color: '#060606' },
@@ -86,8 +91,10 @@ export default defineComponent({
       playPos: (route.query.imagePos || 'left') as 'left' | 'right' // 数字课堂老师端上课 镜像字段
     });
     // 返回
-    const goback = () => {
-      if (route.query.backBtnType === 'microapp') { // microapp 老师端应用里面打开单独处理返回逻辑
+    const goback = (e: any) => {
+      e.stopPropagation();
+      if (route.query.backBtnType === 'microapp') {
+        // microapp 老师端应用里面打开单独处理返回逻辑
         window.parent.postMessage(
           {
             api: 'iframe_exit'
@@ -104,14 +111,35 @@ export default defineComponent({
       postMessage({ api: 'goBack' });
     };
 
+    /* 修改设置的某个值 */
+    const updateSettingValue = (key: string, value: any) => {
+      if (state.settingStatus) {
+        setting_modal[key] = value;
+      } else {
+        setting[key] = value;
+      }
+    };
+    /* 获取设置的某一个值 */
+    const getSettingValue = (key: string) => {
+      let value: any = null;
+      if (state.settingStatus) {
+        value = setting_modal[key];
+      } else {
+        value = setting[key];
+      }
+      return value;
+    };
+
     /** 播放切换 */
     const handlePlay = async () => {
-      if (setting.playState === 'pause') {
-        setting.playState = 'play';
-        if (setting.playType === 'beat') {
-          await handleStartTick();
+      const playState = getSettingValue('playState');
+      const playType = getSettingValue('playType');
+      if (playState === 'pause') {
+        updateSettingValue('playState', 'play');
+        if (playType === 'beat') {
+          await handleStartTick(state.settingStatus);
         } else {
-          await handleStartBeat();
+          await handleStartBeat(state.settingStatus);
         }
       } else {
         handleStop();
@@ -120,19 +148,23 @@ export default defineComponent({
     /** 播放类型 */
     const handlePlayType = () => {
       handleStop();
+      // 判断是否有设置
+
       if (setting.playType === 'beat') {
-        setting.playType = 'tempo';
+        updateSettingValue('playType', 'tempo');
       } else {
-        setting.playType = 'beat';
+        updateSettingValue('playType', 'beat');
       }
     };
 
     const handleStop = () => {
-      setting.playState = 'pause';
-      if (setting.playType === 'beat') {
-        hendleEndTick();
+      const playType = getSettingValue('playType');
+      updateSettingValue('playState', 'pause');
+
+      if (playType === 'beat') {
+        hendleEndTick(state.settingStatus);
       } else {
-        hendleEndBeat();
+        hendleEndBeat(state.settingStatus);
       }
     };
 
@@ -186,7 +218,7 @@ export default defineComponent({
         if (route.query.dataJson) {
           dataJson = JSON.parse(route.query.dataJson as any);
         }
-        console.log(dataJson, 'dataJson', props.dataJson);
+        // console.log(dataJson, 'dataJson', props.dataJson);
 
         setting.element = dataJson.element;
         setting.beat = dataJson.beat;
@@ -196,6 +228,14 @@ export default defineComponent({
         setting.playType = dataJson.playType;
         setting.speed = dataJson.speed;
 
+        setting_modal.element = dataJson.element;
+        setting_modal.beat = dataJson.beat;
+        setting_modal.barLine = dataJson.barLine;
+        setting_modal.tempo = dataJson.tempo;
+        setting_modal.scorePart = dataJson.scorePart;
+        setting_modal.playType = dataJson.playType;
+        setting_modal.speed = dataJson.speed;
+
         state.dataJson = dataJson;
       } catch {
         //
@@ -214,6 +254,21 @@ export default defineComponent({
     //   }
     // );
 
+    /** 打开设置 */
+    const onOpenSetting = () => {
+      handleStop();
+      // 初始化设置的数据
+      for (let i in setting) {
+        setting_modal[i] = JSON.parse(JSON.stringify(setting[i]));
+      }
+
+      if (state.win === 'pc' || state.platform === 'modal') {
+        state.settingPcStatus = true;
+      } else {
+        state.settingStatus = true;
+      }
+    };
+
     onMounted(() => {
       if (route.query.modeType) {
         state.modeType = route.query.modeType;
@@ -243,7 +298,7 @@ export default defineComponent({
       settingBoxDragData = useDrag(
         [`${settingBoxClass} .iconTitBoxMove`, `${settingBoxClass} .bom_drag`],
         settingBoxClass,
-        toRef(state, 'settingStatus'),
+        toRef(state, 'settingPcStatus'),
         stateData.user.data.id
       );
     }
@@ -252,6 +307,7 @@ export default defineComponent({
     return () => (
       <div
         onClick={() => {
+          state.settingStatus = false;
           window.parent.postMessage(
             {
               api: 'clickTempo'
@@ -265,238 +321,303 @@ export default defineComponent({
           state.platform === 'modal' ? styles.modal : '',
           state.modeType === 'courseware' ? styles.courseware : ''
         ]}>
-        <div class={styles.head}>
-          {state.modeType !== 'courseware' && (
-            <div
-              class={[styles.back, styles.iconBack]}
-              onClick={goback}
-              style={{ cursor: 'pointer' }}>
-              <img src={icon_back} />
-            </div>
-          )}
-          <div class={styles.title}>
-            <img src={icon_title} />
-          </div>
-          {state.modeType !== 'courseware' && state.platform !== 'modal' ? (
-            <div
-              class={styles.back}
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                handleStop();
-                state.settingStatus = true;
-              }}>
-              <img src={icon_setting} />
+        <div
+          class={[
+            styles.containerLeft,
+            state.settingStatus ? styles.leftShow : ''
+          ]}>
+          <div class={styles.head}>
+            {state.modeType !== 'courseware' && (
+              <div
+                class={[styles.back, styles.iconBack]}
+                onClick={goback}
+                style={{ cursor: 'pointer' }}>
+                <img src={icon_back} />
+              </div>
+            )}
+            <div class={styles.title}>
+              <img src={icon_title} />
             </div>
-          ) : (
-            <div class={styles.back}></div>
-          )}
-        </div>
-
-        <div class={styles.conCon}>
-          <div class={styles.container}>
-            {setting.scorePart?.map((item: any, i: number) => (
+            {state.modeType !== 'courseware' &&
+            state.platform !== 'modal' &&
+            !state.settingStatus ? (
               <div
-                class={[
-                  styles.beatSection,
-                  setting.scorePart.length >= 2 &&
-                    item.length !== 1 &&
-                    styles.small
-                ]}>
-                {item.map((child: any, jIndex: number) => (
-                  <div
-                    class={[styles.beat, child.selected ? styles.active : '']}
-                    onClick={(e: any) => {
-                      e.stopPropagation();
-                    }}>
-                    <div class={styles.direction}>
-                      <div
-                        class={styles.up}
-                        style={{ cursor: 'pointer' }}
-                        onClick={() => {
-                          if (setting.playState === 'play') return;
-                          if (setting.tempo.length <= 1) {
-                            showToast('无法切换,请选择至少2种节奏型');
-                            return;
-                          }
-                          // const obj = randomScoreElement(child.index);
-                          const obj = elementDirection('up', child.index);
-                          child.index = obj.index;
-                          child.url = obj.url;
-                        }}></div>
-                      <div
-                        class={styles.down}
-                        style={{ cursor: 'pointer' }}
-                        onClick={() => {
-                          if (setting.playState === 'play') return;
-                          if (setting.tempo.length <= 1) {
-                            showToast('无法切换,请选择至少2种节奏型');
-                            return;
-                          }
-                          // const obj = randomScoreElement(child.index);
-                          const obj = elementDirection('down', child.index);
-                          child.index = obj.index;
-                          child.url = obj.url;
-                        }}></div>
-                    </div>
-                    <div class={styles.imgSection}>
-                      <img src={getImage(child.url)} />
-                    </div>
-                  </div>
-                ))}
+                class={styles.back}
+                style={{ cursor: 'pointer' }}
+                onClick={(e: any) => {
+                  e.stopPropagation();
+                  onOpenSetting();
+                }}>
+                <img src={icon_setting} />
               </div>
-            ))}
+            ) : (
+              <div class={styles.back}></div>
+            )}
           </div>
-        </div>
 
-        <div
-          class={styles.footer}
-          onClick={(e: any) => {
-            e.stopPropagation();
-          }}>
-          {/* 播放 */}
-          {state.playPos === 'left' && (
-            <>
-              {route.query.back === 'show' && (
+          <div class={styles.conCon}>
+            <div class={styles.container}>
+              {getSettingValue('scorePart')?.map((item: any, i: number) => (
                 <div
-                  class={[styles.play]}
-                  onClick={goback}
-                  style={{ cursor: 'pointer' }}>
-                  <img src={icon_back1} />
+                  class={[
+                    styles.beatSection,
+                    getSettingValue('scorePart').length >= 2 &&
+                      item.length !== 1 &&
+                      styles.small
+                  ]}>
+                  {item.map((child: any, jIndex: number) => (
+                    <div
+                      class={[
+                        styles.beat,
+                        child.selected ? styles.active : '',
+                        state.settingStatus && styles.disabledChange
+                      ]}
+                      // draggable={true}
+                      onDragenter={(e: any) => {
+                        e.preventDefault();
+                      }}
+                      onDragover={(e: any) => {
+                        e.preventDefault();
+                      }}
+                      onDrop={(e: any) => {
+                        let dropItem = e.dataTransfer.getData('text');
+                        dropItem = dropItem ? JSON.parse(dropItem) : {};
+                        // 判断是否有数据
+                        if (dropItem.url) {
+                          handleStop();
+                          setting_modal.scorePart.forEach(
+                            (part: Array<any>, ci: number) => {
+                              part.forEach((child: any, cj: number) => {
+                                if (i === ci && jIndex === cj) {
+                                  child.url = dropItem.url;
+                                }
+                              });
+                            }
+                          );
+                        }
+                      }}
+                      onClick={(e: any) => {
+                        e.stopPropagation();
+                        // 编辑时可以操作
+                        if (state.settingStatus) {
+                          handleStop();
+                          initSelectScorePartModal();
+                          child.selected = true;
+                        }
+                      }}>
+                      {/* 编辑时不可上下切换 */}
+                      {!state.settingStatus && (
+                        <div class={styles.direction}>
+                          <div
+                            class={styles.up}
+                            style={{ cursor: 'pointer' }}
+                            onClick={() => {
+                              if (setting.playState === 'play') return;
+                              if (setting.tempo.length <= 1) {
+                                showToast('无法切换,请选择至少2种节奏型');
+                                return;
+                              }
+                              // const obj = randomScoreElement(child.index);
+                              const obj = elementDirection('up', child.index);
+                              child.index = obj.index;
+                              child.url = obj.url;
+                            }}></div>
+                          <div
+                            class={styles.down}
+                            style={{ cursor: 'pointer' }}
+                            onClick={() => {
+                              if (setting.playState === 'play') return;
+                              if (setting.tempo.length <= 1) {
+                                showToast('无法切换,请选择至少2种节奏型');
+                                return;
+                              }
+                              // const obj = randomScoreElement(child.index);
+                              const obj = elementDirection('down', child.index);
+                              child.index = obj.index;
+                              child.url = obj.url;
+                            }}></div>
+                        </div>
+                      )}
+                      <div class={styles.imgSection}>
+                        <img src={getImage(child.url)} />
+                      </div>
+                    </div>
+                  ))}
                 </div>
-              )}
-              <div class={styles.play} onClick={handlePlay}>
-                {setting.playState === 'pause' ? (
-                  <img src={iconPause} />
-                ) : (
-                  <img src={iconPlay} />
-                )}
-              </div>
-            </>
-          )}
-          {/* 老师端来的时候的设置按钮 */}
-          {state.platform === 'modal' && state.playPos === 'right' && (
-            <div
-              class={styles.setting}
-              onClick={() => {
-                handleStop();
-                state.settingStatus = true;
-              }}>
-              <img src={setImg} />
+              ))}
             </div>
-          )}
-          {/* 播放类型 */}
-          <div class={styles.playType} onClick={handlePlayType}>
-            {setting.playType === 'beat' ? (
-              <img src={beat} />
-            ) : (
-              <img src={tempo} />
-            )}
           </div>
-          {/* 随机生成 */}
+
           <div
-            class={styles.randomTempo}
-            onClick={() => {
-              renderScore();
-              handleStop();
+            class={styles.footer}
+            onClick={(e: any) => {
+              e.stopPropagation();
             }}>
-            <img src={randDom} />
-          </div>
-          {/* 速度 */}
-          <div class={styles.speedChange}>
-            <img
-              src={iconPlus}
-              class={styles.speedPlus}
-              onClick={() => {
-                if (setting.speed <= 40) return;
-                setting.speed -= 1;
-                handleStop();
-
-                state.speedList.forEach((item: any) => {
-                  if (item.value === setting.speed) {
-                    item.color = '#1CACF1';
-                    setting.speed = setting.speed;
-                  } else {
-                    item.color = '#060606';
-                  }
-                });
-              }}
-            />
-            <Popover
-              placement="top"
-              class={styles.popupContainer}
-              actions={state.speedList}
-              onSelect={(val: any) => {
-                if (val.value === setting.speed) return;
-                state.speedList.forEach((item: any) => {
-                  if (item.value === val.value) {
-                    item.color = '#1CACF1';
-                    setting.speed = val.value;
-                  } else {
-                    item.color = '#060606';
-                  }
-                });
-                handleStop();
-              }}>
-              {{
-                reference: () => (
-                  <div class={styles.speedNum}>{setting.speed}</div>
-                )
-              }}
-            </Popover>
-
-            <img
-              src={iconAdd}
-              class={styles.speedAdd}
-              onClick={() => {
-                if (setting.speed >= 200) return;
-                setting.speed += 1;
-                handleStop();
-
-                state.speedList.forEach((item: any) => {
-                  if (item.value === setting.speed) {
-                    item.color = '#1CACF1';
-                    setting.speed = setting.speed;
-                  } else {
-                    item.color = '#060606';
-                  }
-                });
-              }}
-            />
-          </div>
-          {/* 播放 */}
-          {state.playPos === 'right' && (
-            <div class={styles.play} onClick={handlePlay}>
-              {setting.playState === 'pause' ? (
-                <img src={iconPause} />
+            {/* 播放 */}
+            {state.playPos === 'left' && (
+              <>
+                {route.query.back === 'show' && (
+                  <div
+                    class={[styles.play]}
+                    onClick={goback}
+                    style={{ cursor: 'pointer' }}>
+                    <img src={icon_back1} />
+                  </div>
+                )}
+                <div class={styles.play} onClick={handlePlay}>
+                  {getSettingValue('playState') === 'pause' ? (
+                    <img src={iconPause} />
+                  ) : (
+                    <img src={iconPlay} />
+                  )}
+                </div>
+              </>
+            )}
+            {/* 老师端来的时候的设置按钮 */}
+            {state.platform === 'modal' && state.playPos === 'right' && (
+              <div
+                class={styles.setting}
+                onClick={() => {
+                  onOpenSetting();
+                }}>
+                <img src={setImg} />
+              </div>
+            )}
+            {/* 播放类型 */}
+            <div class={styles.playType} onClick={handlePlayType}>
+              {getSettingValue('playType') === 'beat' ? (
+                <img src={beat} />
               ) : (
-                <img src={iconPlay} />
+                <img src={tempo} />
               )}
             </div>
-          )}
-          {/* 老师端来的时候的设置按钮 */}
-          {state.platform === 'modal' && state.playPos === 'left' && (
+            {/* 随机生成 */}
             <div
-              class={styles.setting}
+              class={styles.randomTempo}
               onClick={() => {
+                if (state.settingStatus) {
+                  renderScoreModal();
+                } else {
+                  renderScore();
+                }
                 handleStop();
-                state.settingStatus = true;
               }}>
-              <img src={setImg} />
+              <img src={randDom} />
             </div>
-          )}
+            {/* 速度 */}
+            <div class={styles.speedChange}>
+              <img
+                src={iconPlus}
+                class={styles.speedPlus}
+                onClick={() => {
+                  const speed = getSettingValue('speed');
+                  if (speed <= 40) return;
+                  updateSettingValue('speed', speed - 1);
+                  handleStop();
+
+                  state.speedList.forEach((item: any) => {
+                    if (item.value === getSettingValue('speed')) {
+                      item.color = '#1CACF1';
+                      updateSettingValue('speed', getSettingValue('speed'));
+                    } else {
+                      item.color = '#060606';
+                    }
+                  });
+                }}
+              />
+              <Popover
+                placement="top"
+                class={styles.popupContainer}
+                actions={state.speedList}
+                onSelect={(val: any) => {
+                  const speed = getSettingValue('speed');
+                  if (val.value === speed) return;
+                  state.speedList.forEach((item: any) => {
+                    if (item.value === val.value) {
+                      item.color = '#1CACF1';
+                      updateSettingValue('speed', val.value);
+                    } else {
+                      item.color = '#060606';
+                    }
+                  });
+                  handleStop();
+                }}>
+                {{
+                  reference: () => (
+                    <div class={styles.speedNum}>
+                      {getSettingValue('speed')}
+                    </div>
+                  )
+                }}
+              </Popover>
+
+              <img
+                src={iconAdd}
+                class={styles.speedAdd}
+                onClick={() => {
+                  const speed = getSettingValue('speed');
+
+                  if (speed >= 200) return;
+                  updateSettingValue('speed', speed + 1);
+                  handleStop();
+
+                  state.speedList.forEach((item: any) => {
+                    if (item.value === getSettingValue('speed')) {
+                      item.color = '#1CACF1';
+                      updateSettingValue('speed', getSettingValue('speed'));
+                    } else {
+                      item.color = '#060606';
+                    }
+                  });
+                }}
+              />
+            </div>
+            {/* 播放 */}
+            {state.playPos === 'right' && (
+              <div class={styles.play} onClick={handlePlay}>
+                {getSettingValue('playState') === 'pause' ? (
+                  <img src={iconPause} />
+                ) : (
+                  <img src={iconPlay} />
+                )}
+              </div>
+            )}
+            {/* 老师端来的时候的设置按钮 */}
+            {state.platform === 'modal' && state.playPos === 'left' && (
+              <div
+                class={styles.setting}
+                onClick={() => {
+                  onOpenSetting();
+                }}>
+                <img src={setImg} />
+              </div>
+            )}
+          </div>
+        </div>
+        <div
+          class={[
+            styles.containerRight,
+            state.settingStatus ? '' : styles.rightHide
+          ]}
+          onClick={(e: any) => {
+            e.stopPropagation();
+          }}>
+          <SettingModal
+            class={styles.settingModalShow}
+            onClose={() => (state.settingStatus = false)}
+          />
         </div>
 
         <Popup
           style={
             state.platform === 'modal' ? settingBoxDragData.styleDrag.value : {}
           }
-          v-model:show={state.settingStatus}
+          v-model:show={state.settingPcStatus}
           class={[styles.settingPopup, settingBoxClass]}>
-          <SettingModal
+          <SettingPcModal
             onGuideDone={setGuidanceShow}
             showGuide={guidanceShow.value}
-            dataJson={state.dataJson}
-            onClose={() => (state.settingStatus = false)}
+            onClose={() => (state.settingPcStatus = false)}
           />
         </Popup>
       </div>

+ 101 - 15
src/views/tempo-practice/setting-modal/index.module.less

@@ -3,24 +3,29 @@
   width: 430px;
   height: 86vh;
   background: #fff;
-  border-radius: 26px;
+  border-radius: 18px 0 0 18px;
   padding: 26px 0 20px;
 
   &.pcS {
     width: 750px;
     height: 536px;
 
-    .paramContent {
-      gap: 18px;
+    .settingContent {
+      padding: 0 20px;
+    }
+
+    .beatContent {
+      gap: 8px 0;
 
       .btn {
         width: 78px;
         height: 31px;
+        margin: 0 4px;
       }
 
       img {
-        width: 80px;
-        height: 80px;
+        width: 64px;
+        height: 64px;
       }
     }
   }
@@ -35,13 +40,15 @@
     height: 34px;
     background: url('../images/icon-set-title.png') no-repeat center center / contain;
   }
-  .iconTitBox{
+
+  .iconTitBox {
     position: absolute;
     top: 0;
     left: 0;
     width: 100%;
-    height: 40px;
-    z-index: 9;
+    height: 0;
+    z-index: 11;
+
     .iconClose {
       position: absolute;
       right: 13px;
@@ -55,12 +62,48 @@
       cursor: pointer;
     }
   }
+
+
+  &.modalS {
+    .beatContent {
+      gap: 8px 0;
+
+      .btn {
+        margin: 0 4px;
+      }
+    }
+
+    .settingContent {
+      padding: 0 10px;
+
+      .iArrow {
+        width: 12px;
+        height: 12px;
+      }
+
+      .collapseContainer {
+        :global {
+          .van-collapse-item__title {
+            font-size: 12px;
+            padding: 4px;
+          }
+
+          .van-collapse-item__content {
+            padding: 10px 3px 0;
+          }
+        }
+
+      }
+    }
+  }
 }
 
 .settingContent {
-  padding: 0 26px;
+  padding: 0 12px 55px 7px;
   overflow-y: auto;
   height: 100%;
+  position: relative;
+  z-index: 10;
 
   &::-webkit-scrollbar {
     width: 4px;
@@ -75,6 +118,38 @@
     border-radius: 0;
     background: rgba(0, 0, 0, 0.1);
   }
+
+  .iArrow {
+    width: 17px;
+    height: 17px;
+    margin-right: 3px;
+  }
+
+  .collapseContainer {
+
+    &.paddingBottom {
+      margin-bottom: 8px;
+    }
+
+    :global {
+      .van-collapse-item__title {
+        display: flex;
+        align-items: center;
+        padding: 8px;
+        background: #E5F0FB;
+        border-radius: 6px;
+        font-weight: 500;
+        font-size: 16px;
+        color: #000000;
+        cursor: pointer;
+      }
+
+      .van-collapse-item__content {
+        padding: 18px 3px 0;
+      }
+    }
+
+  }
 }
 
 .settingParams {
@@ -110,7 +185,7 @@
     background: #F5F6F7;
     border: none;
     padding: 0;
-    margin: 0 6px;
+    margin: 0 4px;
 
     &.active {
       background: #19AEFF;
@@ -118,11 +193,21 @@
     }
   }
 
+  &.beatContent {
+    .btn {
+      margin: 4px 6px;
+    }
+  }
+
   &.tempo {
     // gap: 8px 8px;
     padding-bottom: 0;
     margin: -4px;
 
+    &>div {
+      background: #F5F6F7;
+      margin: 4px;
+    }
   }
 
   .active {
@@ -130,12 +215,13 @@
   }
 
   img {
-    width: 46px;
-    height: 46px;
+    width: 44px;
+    height: 44px;
     background: #F5F6F7;
     border-radius: 4px;
-    margin: 4px;
+    cursor: pointer;
   }
+
 }
 
 .btnGroup {
@@ -143,7 +229,7 @@
   bottom: 0;
   left: 0;
   right: 0;
-  z-index: 9;
+  z-index: 12;
   display: flex;
   align-items: center;
   justify-content: center;
@@ -160,4 +246,4 @@
     background: url('../images/btn-5.png') no-repeat center center / contain;
     border: none;
   }
-}
+}

+ 248 - 110
src/views/tempo-practice/setting-modal/index.tsx

@@ -1,45 +1,64 @@
-import { computed, defineComponent, onMounted, reactive } from 'vue';
+import {
+  PropType,
+  computed,
+  defineComponent,
+  onMounted,
+  reactive,
+  watch
+} from 'vue';
 import styles from './index.module.less';
-import { Button, showToast } from 'vant';
+import { Button, Collapse, CollapseItem, showToast } from 'vant';
 import {
   barLineList,
   beatList,
   elementList,
   renderScore,
+  renderScoreModal,
   setting,
+  setting_modal,
   tempo4,
   tempo8
 } from '../setting';
 import { getImage } from '../images/music';
 import { hendleEndTick } from '../tick';
 import { hendleEndBeat } from '../beat-tick';
-import deepClone from '@/helpers/deep-clone';
 import { useRoute } from 'vue-router';
-import Dragbom from '@/hooks/useDrag/dragbom';
+import settingArrowActive from '../images/setting-arrow-active.png';
+import settingArrowDefault from '../images/setting-arrow-default.png';
+import deepClone from '@/helpers/deep-clone';
 
 export default defineComponent({
-  emits: ['close',"guideDone"],
+  emits: ['close'],
   props: {
-    dataJson: {
-      type: Object,
+    class: {
+      type: Object as PropType<any>,
       default: () => {}
-    },
-    /** 是否显示引导 */
-		showGuide: {
-			type: Boolean,
-			default: false,
-		}
+    }
+    // dataJson: {
+    //   type: Object,
+    //   default: () => {}
+    // }
   },
   name: 'setting-modal',
-  setup(props, { emit }) {
+  setup(props, { emit, expose }) {
     const route = useRoute();
-    const { element, beat, barLine, tempo } = props.dataJson;
+    // const { element, beat, barLine, tempo } = props.dataJson;
+
+    const tempDeepClone = (val: any) => {
+      return JSON.parse(JSON.stringify(val));
+    };
     const state = reactive({
       win: route.query.win,
-      element: element || ('jianpu' as 'jianpu' | 'staff'), // 元素
-      beat: beat || ('4-4' as '4-2' | '4-3' | '4-4' | '8-3' | '8-6'), // 拍号
-      barLine: barLine || ('1' as '1' | '2' | '4'), // 小节数
-      tempo: tempo || (['1', '2', '3'] as any[]) // 节奏形筛选
+      platform: route.query.platform,
+      activeNames: ['base'] as any, // 折叠面板
+      element:
+        tempDeepClone(setting_modal.element) ||
+        ('jianpu' as 'jianpu' | 'staff'), // 元素
+      beat:
+        tempDeepClone(setting_modal.beat) ||
+        ('4-4' as '4-2' | '4-3' | '4-4' | '8-3' | '8-6'), // 拍号
+      barLine: tempDeepClone(setting_modal.barLine) || ('1' as '1' | '2' | '4'), // 小节数
+      tempo: tempDeepClone(setting_modal.tempo) || (['1', '2', '3'] as any[]) // 节奏形筛选
     });
 
     const tempoList = computed(() => {
@@ -51,35 +70,63 @@ export default defineComponent({
       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);
+    // 重置选中数据
+    watch(
+      () => setting_modal,
+      () => {
+        state.element =
+          tempDeepClone(setting_modal.element) ||
+          ('jianpu' as 'jianpu' | 'staff'); // 元素
+        state.beat =
+          tempDeepClone(setting_modal.beat) ||
+          ('4-4' as '4-2' | '4-3' | '4-4' | '8-3' | '8-6'); // 拍号
+        state.barLine =
+          tempDeepClone(setting_modal.barLine) || ('1' as '1' | '2' | '4'); // 小节数
+        state.tempo =
+          tempDeepClone(setting_modal.tempo) || (['1', '2', '3'] as any[]); // 节奏形筛选
+      },
+      {
+        deep: true
       }
+    );
+
+    const getBeatUrl = (value: any) => {
+      const prefix = state.element === 'jianpu' ? 'j-' : 'f-';
+      return prefix + value + '.png';
+    };
+    const onChangeTempo = (item: any) => {
+      setting_modal.scorePart.forEach((part: Array<any>) => {
+        part.forEach((child: any) => {
+          if (child.selected) {
+            child.url = getBeatUrl(item);
+
+            child.selected = false;
+          }
+        });
+      });
     };
 
     const handleStop = () => {
-      setting.playState = 'pause';
-      if (setting.playType === 'beat') {
+      setting_modal.playState = 'pause';
+      if (setting_modal.playType === 'beat') {
         hendleEndTick();
       } else {
         hendleEndBeat();
       }
     };
 
-    const onSubmit = () => {
-      if (state.tempo.length <= 0) {
-        showToast('节奏型不能为空');
-        return;
-      }
+    /** 数据有变化时重置 */
+    const onChangeResetTempo = () => {
+      // if (state.tempo.length <= 0) {
+      //   showToast('节奏型不能为空');
+      //   return;
+      // }
       let status = false; // 是否有更改
       if (
-        setting.element !== state.element ||
-        setting.beat !== state.beat ||
-        setting.barLine !== state.barLine ||
-        setting.tempo.join(',') !== state.tempo.join(',')
+        setting_modal.element !== state.element ||
+        setting_modal.beat !== state.beat ||
+        setting_modal.barLine !== state.barLine ||
+        setting_modal.tempo.join(',') !== state.tempo.join(',')
       ) {
         status = true;
       }
@@ -87,101 +134,192 @@ export default defineComponent({
       // 判断是否有数据变化
       handleStop();
       if (status) {
-        setting.element = JSON.parse(JSON.stringify(state.element));
-        setting.beat = JSON.parse(JSON.stringify(state.beat)); //state.beat;
-        setting.barLine = JSON.parse(JSON.stringify(state.barLine)); // state.barLine;
-        setting.tempo = JSON.parse(JSON.stringify(state.tempo)); // state.tempo;
-        renderScore();
+        setting_modal.element = tempDeepClone(state.element);
+        setting_modal.beat = tempDeepClone(state.beat); //state.beat;
+        setting_modal.barLine = tempDeepClone(state.barLine); // state.barLine;
+        setting_modal.tempo = tempDeepClone(state.tempo); // state.tempo;
+        renderScoreModal();
+      }
+    };
+
+    const onSubmit = () => {
+      // 初始化设置的数据
+      for (let i in setting_modal) {
+        setting[i] = JSON.parse(JSON.stringify(setting_modal[i]));
       }
 
       emit('close');
     };
 
+    expose({
+      onSubmit
+    });
+
     return () => (
       <div
-        class={[styles.settingContainer, state.win === 'pc' ? styles.pcS : '','settingContainer_pc']}>
-        <div class={styles.title}></div>
-        <div class={[styles.iconTitBox,'iconTitBoxMove']}>
+        class={[
+          props.class,
+          styles.settingContainer,
+          state.win === 'pc' ? styles.pcS : '',
+          state.platform === 'modal' && state.win !== 'pc' ? styles.modalS : '',
+          'settingContainer_pc'
+        ]}>
+        {/* <div class={styles.title}></div> */}
+        <div class={[styles.iconTitBox, 'iconTitBoxMove']}>
           <i
             class={styles.iconClose}
             onClick={() => {
               emit('close');
               setTimeout(() => {
-                state.element = JSON.parse(JSON.stringify(setting.element));
-                state.beat = JSON.parse(JSON.stringify(setting.beat)); //state.beat;
-                state.barLine = JSON.parse(JSON.stringify(setting.barLine)); // state.barLine;
-                state.tempo = JSON.parse(JSON.stringify(setting.tempo)); // state.tempo;
+                state.element = tempDeepClone(setting_modal.element);
+                state.beat = tempDeepClone(setting_modal.beat); //state.beat;
+                state.barLine = tempDeepClone(setting_modal.barLine); // state.barLine;
+                state.tempo = tempDeepClone(setting_modal.tempo); // state.tempo;
               }, 300);
             }}></i>
         </div>
 
         <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'];
+          <Collapse v-model={state.activeNames} border={false}>
+            <CollapseItem
+              title="基础设置"
+              name="base"
+              border={false}
+              isLink={false}
+              class={[
+                styles.collapseContainer,
+                state.activeNames.includes('base') ? '' : styles.paddingBottom
+              ]}>
+              {{
+                icon: () => (
+                  <img
+                    src={
+                      state.activeNames.includes('base')
+                        ? settingArrowActive
+                        : settingArrowDefault
                     }
-                  }}>
-                  {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) => (
-                <>
+                    class={styles.iArrow}
+                  />
+                ),
+                default: () => (
+                  <>
+                    <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;
+                            onChangeResetTempo();
+                          }}>
+                          {elementList[item]}
+                        </Button>
+                      ))}
+                    </div>
+                    <div class={styles.parmaTitle}>拍号</div>
+                    <div class={[styles.paramContent, styles.beatContent]}>
+                      {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'];
+                            }
+                            onChangeResetTempo();
+                          }}>
+                          {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;
+                            onChangeResetTempo();
+                          }}>
+                          {barLineList[item]}
+                        </Button>
+                      ))}
+                    </div>
+                  </>
+                )
+              }}
+            </CollapseItem>
+            <CollapseItem
+              title="节奏型"
+              name="beat"
+              border={false}
+              isLink={false}
+              class={styles.collapseContainer}>
+              {{
+                icon: () => (
                   <img
-                    onClick={() => onChangeTempo(item)}
-                    class={state.tempo.includes(item) && styles.active}
-                    src={getImage(
-                      (state.element === 'jianpu' ? 'j-' : 'f-') +
-                        tempoList.value[item]
-                    )}
+                    src={
+                      state.activeNames.includes('beat')
+                        ? settingArrowActive
+                        : settingArrowDefault
+                    }
+                    class={styles.iArrow}
                   />
-                </>
-              ))}
-            </div>
-          </div>
-        </div>
-        <div class={styles.btnGroup}>
-          <Button class={styles.btnSubmit} onClick={onSubmit}></Button>
-          {route.query.platform === 'modal' && <Dragbom showGuide={props.showGuide} onGuideDone={() => emit("guideDone")}></Dragbom>}
+                ),
+                default: () => (
+                  <>
+                    <div class={styles.parmaTitle}>节奏型筛选</div>
+                    <div class={[styles.paramContent, styles.tempo]}>
+                      {Object.keys(tempoList.value).map((item: any) => (
+                        <div
+                          draggable={true}
+                          onDragstart={(e: any) => {
+                            console.log('1111');
+                            e.dataTransfer.setData(
+                              'text',
+                              JSON.stringify({
+                                index: item,
+                                url: getBeatUrl(item)
+                              })
+                            );
+                          }}
+                          onClick={() => onChangeTempo(item)}>
+                          <img
+                            // class={state.tempo.includes(item) && styles.active}
+                            src={getImage(
+                              (state.element === 'jianpu' ? 'j-' : 'f-') +
+                                tempoList.value[item]
+                            )}
+                          />
+                        </div>
+                      ))}
+                    </div>
+                  </>
+                )
+              }}
+            </CollapseItem>
+          </Collapse>
+          {/* <div class={styles.settingParams}></div> */}
         </div>
+        {!state.win && !state.platform && (
+          <div class={styles.btnGroup}>
+            <Button class={styles.btnSubmit} onClick={onSubmit}></Button>
+          </div>
+        )}
       </div>
     );
   }

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

@@ -0,0 +1,222 @@
+.settingPcModal {
+  height: 400px;
+  display: flex;
+  background-color: #fff;
+  border-radius: 16px;
+  overflow: hidden;
+
+  .settingModalShow {
+    height: 100% !important;
+    width: 354px !important;
+  }
+
+  &.modal {
+    height: 300px;
+
+    .conCon {
+      width: 402px;
+    }
+
+    .beatSection {
+      .beat {
+        border-radius: 4px;
+        width: 65px;
+        height: 85px;
+
+        img {
+          width: 48px;
+        }
+      }
+
+      &.small {
+        &:nth-child(2n + 1) {
+          justify-content: flex-end;
+          padding-right: 6px;
+        }
+
+        &:nth-child(2n + 2) {
+          justify-content: flex-start;
+          padding-left: 6px;
+        }
+
+        .beat {
+          width: 35px !important;
+          height: 45px !important;
+          margin: 0 4px;
+
+          img {
+            width: 25px;
+          }
+        }
+      }
+    }
+
+    .settingModalShow {
+      width: 264px !important;
+    }
+
+    .btnSubmit {
+      width: 102px;
+      height: 32px;
+      margin-left: -51px;
+    }
+  }
+}
+
+.conCon {
+  position: relative;
+  flex: 1 auto;
+  display: flex;
+  align-items: center;
+  background: url("../images/pc_bg.png") no-repeat center center / cover;
+  width: 639px;
+
+  .btnSubmit {
+    position: absolute;
+    bottom: 11px;
+    left: 50%;
+    margin-left: -81px;
+    width: 162px;
+    height: 51px;
+    cursor: pointer;
+    z-index: 99;
+  }
+}
+
+.pc {
+  .container {
+    max-width: 1200px;
+    gap: 30px 0;
+  }
+
+  .beatSection {
+    .beat {
+      width: 118px;
+      height: 156px;
+
+      img {
+        width: 140px;
+      }
+    }
+
+    &.small {
+      .beat {
+        width: 139px !important;
+        height: 191px !important;
+
+        img {
+          width: 108px;
+        }
+      }
+    }
+  }
+
+  .footer {
+    padding-bottom: 36px;
+  }
+}
+
+.container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex: 1 auto;
+  flex-wrap: wrap;
+  // gap: 15px 0;
+  // margin-bottom: 15px;
+  max-width: 900px;
+  margin: 0 auto;
+}
+
+.beatSection {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  // margin-bottom: 15px;
+  margin: 7px 0;
+
+  &.small {
+    width: 50%;
+    // margin: 0 16px;
+
+    &:nth-child(2n + 1) {
+      justify-content: flex-end;
+      padding-right: 12px;
+    }
+
+    &:nth-child(2n + 2) {
+      justify-content: flex-start;
+      padding-left: 12px;
+    }
+
+
+    .beat {
+      border: 2px solid #fff;
+      // width: 65px;
+      // height: 86px;
+      width: 60px;
+      height: 72px;
+      cursor: pointer;
+      margin: 0 7px;
+
+      &::before,
+      &::after {
+        width: 19px;
+        height: 5px;
+      }
+
+      img {
+        width: 48px;
+      }
+    }
+  }
+
+  .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;
+    position: relative;
+
+
+    &.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);
+    // }
+  }
+}

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

@@ -0,0 +1,109 @@
+import { defineComponent, reactive, ref } from 'vue';
+import styles from './index.module.less';
+import SettingModal from '../setting-modal';
+import Dragbom from '@/hooks/useDrag/dragbom';
+import { useRoute } from 'vue-router';
+import { initSelectScorePartModal, setting_modal } from '../setting';
+import { getImage } from '../images/music';
+import btn5 from '../images/btn-5.png';
+
+export default defineComponent({
+  name: 'setting-pc-modal',
+  props: {
+    /** 是否显示引导 */
+    showGuide: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['close', 'guideDone'],
+  setup(props, { emit }) {
+    const route = useRoute();
+    const settingModalRef = ref();
+    const state = reactive({
+      platform: route.query.platform,
+      win: route.query.win,
+      activeBeat: ''
+    });
+
+    return () => (
+      <div
+        class={[
+          styles.settingPcModal,
+          state.platform === 'modal' && state.win !== 'pc' ? styles.modal : ''
+        ]}>
+        <div class={styles.conCon}>
+          <div class={styles.container}>
+            {setting_modal.scorePart?.map((item: any, i: number) => (
+              <div
+                class={[
+                  styles.beatSection,
+                  setting_modal.scorePart.length >= 2 &&
+                    item.length !== 1 &&
+                    styles.small
+                ]}>
+                {item.map((child: any, jIndex: number) => (
+                  <div
+                    class={[styles.beat, child.selected ? styles.active : '']}
+                    draggable={false}
+                    onDragenter={(e: any) => {
+                      e.preventDefault();
+                    }}
+                    onDragover={(e: any) => {
+                      e.preventDefault();
+                    }}
+                    onDrop={(e: any) => {
+                      let dropItem = e.dataTransfer.getData('text');
+                      dropItem = dropItem ? JSON.parse(dropItem) : {};
+                      console.log(e, 'event', dropItem);
+                      // 判断是否有数据
+                      if (dropItem.url) {
+                        setting_modal.scorePart.forEach(
+                          (part: Array<any>, ci: number) => {
+                            part.forEach((child: any, cj: number) => {
+                              if (i === ci && jIndex === cj) {
+                                child.url = dropItem.url;
+                              }
+                            });
+                          }
+                        );
+                      }
+                    }}
+                    onClick={(e: any) => {
+                      e.stopPropagation();
+                      initSelectScorePartModal();
+                      child.selected = true;
+                    }}>
+                    <div class={styles.imgSection}>
+                      <img src={getImage(child.url)} />
+                    </div>
+                  </div>
+                ))}
+              </div>
+            ))}
+          </div>
+
+          <img
+            class={styles.btnSubmit}
+            src={btn5}
+            onClick={() => {
+              settingModalRef.value?.onSubmit();
+            }}
+          />
+        </div>
+
+        <SettingModal
+          class={styles.settingModalShow}
+          ref={settingModalRef}
+          onClose={() => emit('close')}
+        />
+
+        {route.query.platform === 'modal' && (
+          <Dragbom
+            showGuide={props.showGuide}
+            onGuideDone={() => emit('guideDone')}></Dragbom>
+        )}
+      </div>
+    );
+  }
+});

+ 129 - 5
src/views/tempo-practice/setting.ts

@@ -1,6 +1,7 @@
 import { reactive } from 'vue';
 import { handleInitTick } from './tick';
 import { handleInitBeat } from './beat-tick';
+import deepClone from '@/helpers/deep-clone';
 
 export const setting = reactive({
   element: 'jianpu' as 'jianpu' | 'staff', // 元素
@@ -11,7 +12,21 @@ export const setting = reactive({
   playState: 'pause' as 'pause' | 'play',
   playType: 'tempo' as 'beat' | 'tempo',
   speed: 60 // 默认速度
-});
+} as any);
+
+/**
+ * 临时设置的数据
+ */
+export const setting_modal = 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, // 生成谱面
+  playState: 'pause' as 'pause' | 'play',
+  playType: 'tempo' as 'beat' | 'tempo',
+  speed: 60 // 默认速度
+} as any);
 
 /** 元素 */
 export const elementList = {
@@ -38,21 +53,33 @@ export const barLineList = {
 /** 节奏型筛选 */
 // 简谱
 const temp: any = {};
+const tempNum = [] as any
 for (let i = 1; i <= 14; i++) {
   temp[i] = i + '.png';
+  tempNum.push(i)
 }
 export const tempo4 = temp;
+export const tempo4Num = tempNum
 
 // 五线谱
 const temp2: any = {};
+const tempNum2 = [] as any
 for (let i = 15; i <= 31; i++) {
   temp2[i] = i + '.png';
+  tempNum2.push(i)
 }
 export const tempo8 = temp2;
+export const tempo8Num = tempNum2
 
 /** 随机生成元素 */
 export const randomScoreElement = (element?: string) => {
-  const tempoList = setting.tempo;
+  // const tempoList = setting.tempo;
+  let tempoList = tempo4Num || [] as any
+  if (['4-2', '4-3', '4-4'].includes(setting_modal.beat)) {
+    tempoList = tempo4Num;
+  } else if (['8-3', '8-6'].includes(setting_modal.beat)) {
+    tempoList = tempo8Num;
+  }
   const prefix = setting.element === 'jianpu' ? 'j-' : 'f-';
   if (element) {
     const newArr = tempoList.filter((item: any) => item !== element);
@@ -85,7 +112,13 @@ export const elementDirection = (type: string, index: number) => {
   const prefix = setting.element === 'jianpu' ? 'j-' : 'f-';
   let ele = '';
   let i = 0;
-  const tempoList = setting.tempo;
+  // const tempoList = setting.tempo;
+  let tempoList = tempo4Num || [] as any
+  if (['4-2', '4-3', '4-4'].includes(setting_modal.beat)) {
+    tempoList = tempo4Num;
+  } else if (['8-3', '8-6'].includes(setting_modal.beat)) {
+    tempoList = tempo8Num;
+  }
   const toIndex = tempoList.findIndex((t: any) => Number(t) === index);
   if (type === 'up') {
     if (toIndex <= 0) {
@@ -113,7 +146,10 @@ export const elementDirection = (type: string, index: number) => {
   };
 };
 
-/** 生成谱面 */
+/**
+ * 生成谱面
+ * @param {temp} 是否临时生成
+ * */
 export const renderScore = () => {
   const barLine = Number(setting.barLine);
   const beatA = setting.beat.split('-').map(i => Number(i));
@@ -121,7 +157,6 @@ export const renderScore = () => {
   if (beatA[0] === 8) {
     beat = beat / 3;
   }
-  console.log(beat, 'beat');
   const tempBeat: any = [];
   for (let i = 0; i < barLine; i++) {
     tempBeat[i] = [];
@@ -131,6 +166,7 @@ export const renderScore = () => {
       };
     }
   }
+
   setting.scorePart = tempBeat;
 
   const beatLengthInMilliseconds = (60 / setting.speed) * 1000;
@@ -144,6 +180,9 @@ export const renderScore = () => {
     handleInitBeat(beatLengthInMilliseconds, Number(beatA[1]) || 4);
   }
   initSelectScorePart();
+
+  // 初始化设置里面的数据
+  setting_modal.scorePart = deepClone(setting.scorePart)
 };
 
 /** 初始化选中状态 */
@@ -158,3 +197,88 @@ export const initSelectScorePart = (i?: number, j?: number) => {
     setting.scorePart[i][j].selected = true;
   }
 };
+
+
+
+
+/** 随机生成元素  设置中 */
+export const randomScoreElementModal = (element?: string) => {
+  // const tempoList = setting.tempo;
+  let tempoList = tempo4Num || [] as any
+  if (['4-2', '4-3', '4-4'].includes(setting_modal.beat)) {
+    tempoList = tempo4Num;
+  } else if (['8-3', '8-6'].includes(setting_modal.beat)) {
+    tempoList = tempo8Num;
+  }
+
+  const prefix = setting_modal.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 renderScoreModal = () => {
+  const barLine = Number(setting_modal.barLine);
+  const beatA = setting_modal.beat.split('-').map(i => Number(i));
+  let beat = beatA[1];
+  if (beatA[0] === 8) {
+    beat = beat / 3;
+  }
+  const tempBeat: any = [];
+  for (let i = 0; i < barLine; i++) {
+    tempBeat[i] = [];
+    for (let j = 0; j < beat; j++) {
+      tempBeat[i][j] = {
+        ...randomScoreElementModal()
+      };
+    }
+  }
+
+  setting_modal.scorePart = tempBeat;
+
+  const beatLengthInMilliseconds = (60 / setting.speed) * 1000;
+  if (setting_modal.playType === 'beat') {
+    handleInitTick(
+      beatLengthInMilliseconds,
+      Number(beatA[1]) || 4,
+      Number(beatA[0])
+    );
+  } else {
+    handleInitBeat(beatLengthInMilliseconds, Number(beatA[1]) || 4);
+  }
+  initSelectScorePartModal();
+};
+
+/** 初始化选中状态 设置中 */
+export const initSelectScorePartModal = (i?: number, j?: number) => {
+  setting_modal.scorePart.forEach((part: Array<any>) => {
+    part.forEach((item: any) => {
+      item.selected = false;
+    });
+  });
+
+  if (i !== undefined && j !== undefined && setting_modal.scorePart[i][j]) {
+    setting_modal.scorePart[i][j].selected = true;
+  }
+};

+ 47 - 14
src/views/tempo-practice/tick.ts

@@ -1,7 +1,7 @@
 import { reactive } from 'vue';
 import tockAndTick from './tockAndTick.json';
 import { Howl } from 'howler';
-import { initSelectScorePart, setting } from './setting';
+import { initSelectScorePart, initSelectScorePartModal, setting, setting_modal } from './setting';
 import { beatDesc } from './beat-desc';
 
 const tickData = reactive({
@@ -17,6 +17,12 @@ const tickData = reactive({
   index: 0,
   show: false
 });
+// console.log(setting, 'setting')
+let defaultSetting = {
+  // playState: setting.playState,
+  // speed: setting.speed,
+  // scorePart: setting.scorePart
+} as any
 
 let diffTime = 0; // 每一次节拍时间差
 const handlePlay = (i: number, source: any) => {
@@ -28,10 +34,14 @@ const handlePlay = (i: number, source: any) => {
     }
     let timeSppedEnum = 16.7;
     const proofTime = () => {
-      if (setting.playState !== 'play') {
+      if (defaultSetting.playState !== 'play') {
         return;
       }
       setTimeout(() => {
+        if (tickData.tickEnd) {
+          resolve(i);
+          return;
+        }
         const currentTime = new Date().getTime();
         // 两次定时任务时间间隔
         const diffTime = currentTime - payBeatTime;
@@ -84,8 +94,17 @@ export const handleInitTick = (
   tickData.afterBeat = afterBeat;
 };
 
-/** 开始节拍器 */
-export const handleStartTick = async () => {
+/** 开始节拍器
+ * @param {boolean} 是否开启了设置
+*/
+export const handleStartTick = async (settingStatus = false) => {
+  const tempSetting = settingStatus ? setting_modal : setting
+  defaultSetting = {
+    playState: tempSetting.playState,
+    speed: tempSetting.speed,
+    scorePart: tempSetting.scorePart
+  }
+
   tickData.show = true;
   tickData.tickEnd = false;
   if (tickData.state !== 'ok') {
@@ -98,15 +117,15 @@ export const handleStartTick = async () => {
     tickData.state = 'ok';
   }
   tickData.index = 0;
-  tickData.beatLengthInMilliseconds = (60 / setting.speed) * 1000;
+  tickData.beatLengthInMilliseconds = (60 / defaultSetting.speed) * 1000;
 
   if (tickData.afterBeat === 8) {
     tickData.beatLengthInMilliseconds = tickData.beatLengthInMilliseconds * 0.5;
   }
 
-  for (let i = 0; i < setting.scorePart.length; i++) {
+  for (let i = 0; i < defaultSetting.scorePart.length; i++) {
     if (tickData.tickEnd) return false;
-    const temp = setting.scorePart[i];
+    const temp = defaultSetting.scorePart[i];
     let len = temp.length;
     if (tickData.afterBeat === 8) {
       len = tickData.len;
@@ -119,22 +138,36 @@ export const handleStartTick = async () => {
         j === 0 ? tickData.source1 : j === len ? null : tickData.source2;
 
       await handlePlay(j, source);
-      if (tickData.afterBeat === 8) {
-        initSelectScorePart(i, Math.floor((j <= 0 ? 1 : j) / 3));
+
+      const tempJ = tickData.afterBeat === 8 ? Math.floor((j <= 0 ? 1 : j) / 3) : j
+      if (settingStatus) {
+        initSelectScorePartModal(i, tempJ)
       } else {
-        initSelectScorePart(i, j);
+        initSelectScorePart(i, tempJ);
+      }
+
+      defaultSetting.playState = settingStatus ? setting_modal.playState : setting.playState
+      if (defaultSetting.playState !== 'play') {
+        hendleEndTick(settingStatus)
       }
     }
   }
 
   // console.log(+new Date() - startTime);
   tickData.show = false;
-  handleStartTick();
+  handleStartTick(settingStatus);
   return true;
 };
 
-/** 节拍器暂停 */
-export const hendleEndTick = () => {
+/** 节拍器暂停
+ * @param {boolean} 是否开启了设置
+ */
+export const hendleEndTick = (settingStatus = false) => {
   tickData.tickEnd = true;
-  initSelectScorePart();
+  // 添加延期是因为播放时定时任务播放最多有16.7毫秒的延迟
+  if (settingStatus) {
+    initSelectScorePartModal()
+  } else {
+    initSelectScorePart();
+  }
 };