瀏覽代碼

Merge branch 'iteration-orchestra-new' into online

lex 1 年之前
父節點
當前提交
9af7a4846c
共有 23 個文件被更改,包括 2075 次插入59 次删除
  1. 6 0
      src/components/o-header/index.tsx
  2. 17 1
      src/router/routes-teacher.ts
  3. 二進制
      src/views/unit-test/unit-create/image/icon-delete.png
  4. 二進制
      src/views/unit-test/unit-create/image/icon-delete2.png
  5. 二進制
      src/views/unit-test/unit-create/image/icon-edit.png
  6. 二進制
      src/views/unit-test/unit-create/image/icon-menu.png
  7. 二進制
      src/views/unit-test/unit-create/image/icon-music.png
  8. 二進制
      src/views/unit-test/unit-create/image/icon-radio-checked.png
  9. 二進制
      src/views/unit-test/unit-create/image/icon-radio-default.png
  10. 21 2
      src/views/unit-test/unit-create/modals/newspaper-item.module.less
  11. 93 36
      src/views/unit-test/unit-create/modals/newspaper-item.tsx
  12. 44 3
      src/views/unit-test/unit-create/uni-last.module.less
  13. 78 11
      src/views/unit-test/unit-create/uni-last.tsx
  14. 212 0
      src/views/unit-test/unit-create/unit-edit-test/add-unit-item.module.less
  15. 543 0
      src/views/unit-test/unit-create/unit-edit-test/add-unit-item.tsx
  16. 40 0
      src/views/unit-test/unit-create/unit-edit-test/data.ts
  17. 254 0
      src/views/unit-test/unit-create/unit-edit-test/index.module.less
  18. 306 0
      src/views/unit-test/unit-create/unit-edit-test/index.tsx
  19. 154 0
      src/views/unit-test/unit-create/unit-edit-test/music-list.module.less
  20. 302 0
      src/views/unit-test/unit-create/unit-edit-test/music-list.tsx
  21. 3 4
      src/views/unit-test/unit-detail/index.tsx
  22. 1 1
      src/views/unit-test/unit-list/index.tsx
  23. 1 1
      vite.config.ts

+ 6 - 0
src/components/o-header/index.tsx

@@ -49,6 +49,11 @@ export default defineComponent({
       type: Function,
       default: () => ({})
     },
+    onClickBack: {
+      // 点击左侧箭头
+      type: Function,
+      default: () => ({})
+    },
     desotry: {
       // 用于组件销毁时,是否重置原生头部样式
       type: Boolean,
@@ -99,6 +104,7 @@ export default defineComponent({
       !browser().isApp && callBack && callBack()
     },
     onClickLeft() {
+      this.onClickBack && this.onClickBack()
       if (browser().isApp) {
         postMessage({ api: 'goBack' })
       } else {

+ 17 - 1
src/router/routes-teacher.ts

@@ -136,6 +136,22 @@ export default [
         }
       },
       {
+        path: '/unit-edit-test',
+        name: 'unit-edit-test',
+        component: () => import('@/views/unit-test/unit-create/unit-edit-test'),
+        meta: {
+          title: '编辑内容'
+        }
+      },
+      {
+        path: '/add-unit-item',
+        name: 'add-unit-item',
+        component: () => import('@/views/unit-test/unit-create/unit-edit-test/add-unit-item'),
+        meta: {
+          title: '添加测验曲目'
+        }
+      },
+      {
         path: '/unitDetail',
         name: 'unitDetail',
         component: () => import('@/views/unit-test/unit-list/unitDetail'),
@@ -150,7 +166,7 @@ export default [
         meta: {
           title: '补助明细'
         }
-      },
+      }
 
       //unitDetail 选择阶段自测
     ]

二進制
src/views/unit-test/unit-create/image/icon-delete.png


二進制
src/views/unit-test/unit-create/image/icon-delete2.png


二進制
src/views/unit-test/unit-create/image/icon-edit.png


二進制
src/views/unit-test/unit-create/image/icon-menu.png


二進制
src/views/unit-test/unit-create/image/icon-music.png


二進制
src/views/unit-test/unit-create/image/icon-radio-checked.png


二進制
src/views/unit-test/unit-create/image/icon-radio-default.png


+ 21 - 2
src/views/unit-test/unit-create/modals/newspaper-item.module.less

@@ -3,6 +3,7 @@
   background: #ffffff;
   border-radius: 10px;
   padding-top: 12px;
+
   .titleWrap {
     padding: 0 12px;
     height: 22px;
@@ -11,6 +12,7 @@
     color: #333333;
     line-height: 22px;
   }
+
   .itemBottom {
     margin-top: 15px;
     display: flex;
@@ -19,15 +21,18 @@
     justify-content: space-around;
     text-align: center;
     padding: 0 12px;
+
     .itemBottomDot {
       flex-basis: 33.33%;
       position: relative;
+
       .dotMain {
-        font-size: 26px;
+        font-size: 24px;
         color: #333333;
         line-height: 30px;
         margin-bottom: 4px;
-        font-family: 'DINA' !important;
+        font-family: DINAlternate, DINAlternate;
+        font-weight: bold;
 
         span {
           margin-left: 1px;
@@ -37,6 +42,7 @@
           line-height: 17px;
         }
       }
+
       .dotSub {
         font-size: 12px;
         font-weight: 400;
@@ -45,6 +51,7 @@
       }
     }
   }
+
   .uniTimeWrap {
     padding: 12px;
     margin-top: 20px;
@@ -53,12 +60,14 @@
     align-items: center;
     justify-content: space-between;
     border-top: 1px solid #f2f2f2;
+
     .uniTimeWrapLeft {
       display: flex;
       flex-direction: row;
       align-items: center;
       font-size: 14px;
       line-height: 20px;
+
       .clockIcon {
         margin-top: -2px;
         font-size: 18px;
@@ -67,3 +76,13 @@
     }
   }
 }
+
+.editBtn {
+  color: #F67146;
+  background-color: #FFF5F2;
+  border-color: #FFB39B;
+  font-size: 13px;
+  height: 30px;
+  padding-top: 2px;
+  line-height: 28px;
+}

+ 93 - 36
src/views/unit-test/unit-create/modals/newspaper-item.tsx

@@ -1,59 +1,116 @@
-import { defineComponent, reactive, ref } from 'vue'
+import { computed, defineComponent, reactive, ref } from 'vue'
 import styles from './newspaper-item.module.less'
 import msgIcon from '@/school/images/msg-icon.png'
 import sendmsgIcon from '@/school/images/sendmsg-icon.png'
 import phoneIcon from '@/school/images/phone-icon.png'
 import { postMessage } from '@/helpers/native-message'
 import clockIcon from '@/school/attendance/images/clock-icon.png'
-import { Icon, ActionSheet } from 'vant'
+import { Icon, ActionSheet, Button } from 'vant'
+import { useRouter } from 'vue-router'
+import { unitData } from '../unit-edit-test/data'
 
 export default defineComponent({
   props: ['item'],
   name: 'newspaper-item',
 
   setup(props) {
+    const router = useRouter()
     const gotoDetail = () => {
       // window.open()
     }
+
+    const wTotalScore = computed(() => {
+      let countScore = 0
+      if (props.item?.level) {
+        const aQuestionList = unitData['level' + props.item.level]?.questionList || []
+        countScore = props.item.totalScore
+        aQuestionList.forEach((s: any) => {
+          countScore += Number(s.question.totalScore) || 0
+        })
+        unitData['level' + props.item.level].totalScore = countScore
+      }
+
+      return countScore
+    })
+
+    // 重新计算题目数
+    const wQuestionNum = computed(() => {
+      let aQuestionList: any = []
+      if (props.item?.level) {
+        aQuestionList = unitData['level' + props.item.level]?.questionList || []
+      }
+      return (props.item?.questionNum || 0) + aQuestionList.length
+    })
+
+    // 合格分
+    const wPassScore = computed(() => {
+      let score = 0
+      if (props.item?.level) {
+        const level1 = unitData['level' + props.item.level]
+        if (level1.passScore > 0) {
+          score = level1.passScore
+        } else {
+          score = props.item?.passScore
+        }
+      }
+      return score
+    })
+
+    // 测验时间
+    const wTimeMinutes = computed(() => {
+      let timeMinutes = 0
+      if (props.item?.level) {
+        const level1 = unitData['level' + props.item.level]
+        if (level1.timeMinutes > 0) {
+          timeMinutes = level1.timeMinutes
+        } else {
+          timeMinutes = props.item?.timeMinutes
+        }
+      }
+      return timeMinutes
+    })
+    //
+    const onEdit = () => {
+      //
+      router.push({
+        path: '/unit-edit-test',
+        query: {
+          ...props.item
+        }
+      })
+    }
     return () => (
-      <>
-        <div>
-          <div class={styles.itemWrap} onClick={gotoDetail}>
-            <div class={styles.titleWrap}>{props.item?.unitExaminationName}</div>
-            <div class={styles.itemBottom}>
-              <div class={[styles.itemBottomDot, 'gridBorderRight']}>
-                <p class={styles.dotMain}>
-                  {props.item?.totalScore || 0}
-                  {/* <span>分</span> */}
-                </p>
-                <p class={styles.dotSub}> 总分</p>
-              </div>
-              <div class={[styles.itemBottomDot, 'gridBorderRight']}>
-                <p class={styles.dotMain} style={{ color: '#F67146' }}>
-                  {props.item?.passScore || 0}
-                  {/* <span>分</span> */}
-                </p>
-                <p class={styles.dotSub}>合格分 </p>
-              </div>
-              <div class={styles.itemBottomDot}>
-                <p class={styles.dotMain}>
-                  {props.item?.questionNum || 0}
-                  {/* <span>分</span>{' '} */}
-                </p>
-                <p class={styles.dotSub}>题目数量 </p>
-              </div>
+      <div>
+        <div class={styles.itemWrap} onClick={gotoDetail}>
+          <div class={styles.titleWrap}>{props.item?.unitExaminationName}</div>
+          <div class={styles.itemBottom}>
+            <div class={[styles.itemBottomDot, 'gridBorderRight']}>
+              <p class={styles.dotMain}>{wTotalScore.value || 0}</p>
+              <p class={styles.dotSub}> 总分</p>
+            </div>
+            <div class={[styles.itemBottomDot, 'gridBorderRight']}>
+              <p class={styles.dotMain} style={{ color: '#F67146' }}>
+                {wPassScore.value || 0}
+              </p>
+              <p class={styles.dotSub}>合格分 </p>
             </div>
-            <div class={styles.uniTimeWrap}>
-              <div class={styles.uniTimeWrapLeft}>
-                {' '}
-                <Icon name={clockIcon} class={styles.clockIcon}></Icon> 测验时长:
-                <span style={{ color: '#F67146' }}>{props.item?.timeMinutes || 0} </span> 分钟
-              </div>
-              {/* <Icon name="arrow" color="#777"></Icon> */}
+            <div class={styles.itemBottomDot}>
+              <p class={styles.dotMain}>{wQuestionNum.value || 0}</p>
+              <p class={styles.dotSub}>题目数量 </p>
             </div>
           </div>
+          <div class={styles.uniTimeWrap}>
+            <div class={styles.uniTimeWrapLeft}>
+              <Icon name={clockIcon} class={styles.clockIcon}></Icon> 测验时长:
+              <span style={{ color: '#F67146' }}>{wTimeMinutes.value || 0} </span> 分钟
+            </div>
+
+            <Button plain round class={styles.editBtn} onClick={onEdit}>
+              编辑内容
+            </Button>
+          </div>
         </div>
-      </>
+      </div>
     )
   }
 })

+ 44 - 3
src/views/unit-test/unit-create/uni-last.module.less

@@ -10,15 +10,18 @@
     margin-bottom: 15px;
     line-height: 22px;
     font-size: 16px;
+
     .editIcon {
       font-size: 18px;
       margin-right: 6px;
     }
   }
+
   .infoWrap {
     background-color: #fff;
     border-radius: 10px;
     padding: 8px 0px;
+
     p {
       font-size: 15px;
       font-weight: 500;
@@ -26,22 +29,44 @@
       line-height: 21px;
       margin-bottom: 15px;
     }
+
     .loctionIconWrap {
-      margin-left: 5px;
+      // margin-left: 5px;
+
       span {
         line-height: 22px;
         vertical-align: inherit;
       }
     }
+
     .wrapValue {
       font-weight: 500;
       color: #333333;
       line-height: 21px;
     }
+
+    .inputControl {
+      :global {
+        .van-field__body {
+          justify-content: flex-end;
+          margin-right: 8px;
+        }
+
+        .van-field__control {
+          width: 58px;
+          height: 33px;
+          background: #F4F4F4;
+          border-radius: 8px;
+          text-align: center;
+        }
+      }
+    }
+
     .heightValue {
       padding-bottom: 15px !important;
       margin-bottom: 8px;
     }
+
     input::-webkit-input-placeholder,
     input::-ms-input-placeholder {
       font-size: 15px;
@@ -52,6 +77,7 @@
         padding: 8px 12px;
         font-size: 15px;
       }
+
       .van-cell__title {
         font-size: 15px;
         font-weight: 500;
@@ -60,11 +86,14 @@
         margin-bottom: 15px;
         flex: 0 auto;
       }
+
       .van-field__control {
-        color: #f67146;
+        color: #333;
+        font-weight: bold;
       }
     }
   }
+
   .tabsWrap {
     position: relative;
     margin-top: 10px;
@@ -74,6 +103,7 @@
         .van-tabs__nav {
           padding-left: 0;
           padding-right: 0;
+
           .van-tab:first-child {
             padding-left: 0;
           }
@@ -88,11 +118,13 @@
       z-index: 2000;
     }
   }
+
   .DialogTitle {
     display: flex;
     flex-direction: row;
     align-items: center;
     padding: 20px 25px;
+
     span {
       width: 4px;
       height: 14px;
@@ -100,6 +132,7 @@
       border-radius: 2px;
       margin-right: 6px;
     }
+
     p {
       height: 25px;
       font-size: 18px;
@@ -108,33 +141,41 @@
       line-height: 25px;
     }
   }
+
   .DialogConent {
     padding: 0 25px 30px;
+
     p {
       font-size: 16px;
       line-height: 26px;
       color: #333;
     }
   }
+
   :global {
     .van-tab__text {
       font-size: 16px;
     }
+
     .van-tabs__line {
       bottom: 20px;
       width: 20px;
     }
+
     .van-cell__title {
       margin-bottom: 0 !important;
     }
+
     .exercisDetailDialog {
       border-radius: 10px;
+
       .van-dialog__header {
         padding-top: 0px !important;
       }
     }
+
     .van-dialog {
       top: 47%;
     }
   }
-}
+}

+ 78 - 11
src/views/unit-test/unit-create/uni-last.tsx

@@ -23,6 +23,8 @@ import NewspaperItem from './modals/newspaper-item'
 import questIcon from '@/school/images/quest-icon.png'
 import { postMessage } from '@/helpers/native-message'
 import OHeader from '@/components/o-header'
+import OEmpty from '@/components/o-empty'
+import { resestState, unitData } from './unit-edit-test/data'
 // import { browser } from '@/helpers/utils'
 export default defineComponent({
   name: 'uni-test',
@@ -65,6 +67,8 @@ export default defineComponent({
           state.platformApi + `/classGroup/detail/${forms.value.classGroupId}`
         )
         forms.value.preStudentNum = data.preStudentNum || 0
+
+        unitData.courseType = data.courseType // 缓存班级类型
       } catch (e) {
         console.log(e)
       }
@@ -94,6 +98,7 @@ export default defineComponent({
         forms.value.lessonCoursewareId = data.lessonCoursewareExaminationMapper.lessonCoursewareId
         forms.value.preStudentNum = data.studentNum || 0
         forms.value.testId = data.lessonCoursewareExaminationMapper.id
+        unitData.courseType = data.courseType // 缓存班级类型
 
         const details = data.lessonCoursewareExaminationMapper.details || []
         details.forEach((item: any) => {
@@ -108,7 +113,12 @@ export default defineComponent({
 
     onMounted(() => {
       forms.value = { ...JSON.parse(sessionStorage.getItem('unit-create') || '{}') } as any
-      console.log(forms.value)
+
+      // 缓存的截止日期
+      if (unitData.expiryDate) {
+        forms.value.expiryDate = unitData.expiryDate
+      }
+      activeName.value = unitData.level
       const query = route.query
       // api-teacher/lessonCoursewareExaminationMapper/detailByCourseId
       // 判断是从课程结束后,还是正常创建
@@ -147,20 +157,58 @@ export default defineComponent({
         return
       }
       try {
+        // 合并参数
+        const params = {
+          classGroupId: forms.value.classGroupId,
+          lessonCoursewareExaminationMapperId: forms.value.testId,
+          unitExaminationName: `${forms.value?.coursewareName}-${forms.value?.testName}`,
+          expiryDate: forms.value.expiryDate,
+          unitConfig: [] as any,
+          questionList: [] as any
+        } as any
+
+        for (const i in datas.uniDetail) {
+          const details = datas.uniDetail[i]
+          const userUnit = unitData['level' + i]
+          params.unitConfig.push({
+            level: i,
+            passScore: userUnit.passScore > 0 ? userUnit.passScore : details.passScore,
+            timeMinutes: userUnit.timeMinutes > 0 ? userUnit.timeMinutes : details.timeMinutes
+          })
+
+          const userUnitQuestion = userUnit.questionList || []
+
+          userUnitQuestion.forEach((unit: any) => {
+            const questionExtendsInfo = unit.question.questionExtendsInfo
+              ? JSON.parse(unit.question.questionExtendsInfo)
+              : {}
+
+            params.questionList.push({
+              musicSheetId: unit.question.mediaUrls,
+              musicName: unit.question.name,
+              questionTypeCode: 'PLAY',
+              difficultyCoefficient: 'ONE',
+              level: i,
+              score: questionExtendsInfo.score,
+              totalScore: unit.question.totalScore,
+              difficulty: questionExtendsInfo.difficulty,
+              start: questionExtendsInfo.start,
+              end: questionExtendsInfo.end,
+              mediaUrls: unit.question.mediaUrls
+            })
+          })
+        }
+
         await request.post(
           state.platformApi + `/classGroupUnitExamination/publishUnitExamination`,
           {
             hideLoading: false,
-            data: {
-              classGroupId: forms.value.classGroupId,
-              lessonCoursewareExaminationMapperId: forms.value.testId,
-              unitExaminationName: `${forms.value?.coursewareName}-${forms.value?.testName}`,
-              expiryDate: forms.value.expiryDate
-            }
+            data: params
           }
         )
         showToast('创建成功')
         sessionStorage.removeItem('unit-create')
+        resestState()
         // 如果连接上面有班级编号,说明当前页面是从原生来的
         if (route.query.classGroupId) {
           postMessage({ api: 'back' })
@@ -174,7 +222,11 @@ export default defineComponent({
     return () => (
       <>
         <div class={styles.lastWrap}>
-          <OHeader />
+          <OHeader
+            onClickBack={() => {
+              resestState()
+            }}
+          />
           <h4 class={styles.uniTitle}>
             <Icon name={iconEdit} class={styles.editIcon}></Icon>
             {forms.value?.coursewareName}-{forms.value?.testName}
@@ -203,8 +255,12 @@ export default defineComponent({
             />
             <Field
               v-model={forms.value.expiryDate}
+              onUpdate:modelValue={() => {
+                unitData.expiryDate = forms.value.expiryDate
+              }}
               type="number"
-              placeholder="请输入截止日期"
+              center
+              class={styles.inputControl}
               input-align="right"
               label="截止时间"
               maxlength={3}
@@ -230,6 +286,9 @@ export default defineComponent({
             />
             <Tabs
               v-model:active={activeName.value}
+              onUpdate:active={() => {
+                unitData.level = activeName.value
+              }}
               class={styles.rankTabs}
               background={'#F8F8F8'}
               title-active-color={'#333333'}
@@ -241,10 +300,18 @@ export default defineComponent({
                 <NewspaperItem item={datas.uniDetail[1]}></NewspaperItem>
               </Tab>
               <Tab name="two" title="双团学员">
-                <NewspaperItem item={datas.uniDetail[2]}></NewspaperItem>
+                {datas.uniDetail[2]?.unitExaminationId ? (
+                  <NewspaperItem item={datas.uniDetail[2]} />
+                ) : (
+                  <OEmpty tips="暂无测验" />
+                )}
               </Tab>
               <Tab name="three" title="多团学员">
-                <NewspaperItem item={datas.uniDetail[3]}></NewspaperItem>
+                {datas.uniDetail[3]?.unitExaminationId ? (
+                  <NewspaperItem item={datas.uniDetail[3]} />
+                ) : (
+                  <OEmpty tips="暂无测验" />
+                )}
               </Tab>
             </Tabs>
           </div>

+ 212 - 0
src/views/unit-test/unit-create/unit-edit-test/add-unit-item.module.less

@@ -0,0 +1,212 @@
+.addUnitItem {
+  min-height: 100vh;
+  overflow: hidden;
+
+  :global {
+    .van-dialog__message {
+      font-size: 16px;
+      color: #333;
+      padding-top: 18px
+    }
+  }
+
+  .disabled {
+    opacity: 0.6;
+    cursor: not-allowed;
+    pointer-events: none;
+  }
+}
+
+.dialogDelete {
+  display: flex;
+  align-items: center;
+  padding-left: var(--van-dialog-message-padding);
+
+  &::before {
+    content: '';
+    display: inline-block;
+    width: 4px;
+    height: 14px;
+    background: #FF8057;
+    border-radius: 2px;
+    margin-right: 6px;
+  }
+}
+
+
+
+.addBtn {
+  display: block;
+  padding: 0 48px;
+  font-size: 15px;
+  font-family: PingFangSC, PingFang SC;
+  font-weight: 500;
+  color: #F67146;
+  background: #FFF5F2;
+  border-radius: 20px;
+  border: 1px solid #FFB39B;
+  height: 39px;
+  margin: 15px auto;
+
+  :global {
+    .van-icon {
+      font-size: 16px;
+      font-weight: bold;
+      margin-left: 6px;
+    }
+  }
+}
+
+.cellGroup {
+  margin: 12px 13px;
+  border-radius: 10px;
+  overflow: hidden;
+
+  .iconMusic {
+    font-size: 18px;
+    margin-right: 6px;
+  }
+
+  .iconDelete {
+    font-size: 20px;
+  }
+
+  .title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333333;
+    line-height: 22px;
+  }
+
+  :global {
+    .van-cell {
+      padding: 16px 12px;
+      font-size: 15px;
+    }
+
+    .van-field__label {
+      color: #777777;
+    }
+  }
+
+  .inputControl {
+    :global {
+      .van-field__body {
+        justify-content: flex-end;
+        margin-right: 8px;
+      }
+
+      .van-field__control {
+        width: 58px;
+        height: 33px;
+        background: #F4F4F4;
+        border-radius: 8px;
+        text-align: center;
+      }
+    }
+  }
+}
+
+.partPopup {
+  :global {
+    .van-popup__close-icon {
+      font-size: 19px;
+      top: 23px;
+      right: 20px;
+    }
+  }
+}
+
+.partContainer {
+  .partTitle {
+    font-size: 18px;
+    font-weight: 500;
+    color: #131415;
+    line-height: 25px;
+    text-align: center;
+    padding: 20px 0;
+  }
+
+
+  .activeCell {
+    .partContent {
+      span {
+        color: #131415;
+      }
+    }
+
+    :global {
+      .van-cell__title {
+        color: #F67146;
+      }
+
+      .van-stepper__input {
+        text-align: center;
+        color: #131415;
+      }
+    }
+  }
+
+  :global {
+    .van-cell {
+      padding: 20px 15px;
+    }
+
+    .van-radio {
+      align-items: flex-start;
+      margin-top: 3px;
+
+      img {
+        width: 18px;
+        height: 18px;
+      }
+    }
+
+    .van-cell__title {
+      font-size: 16px;
+      font-weight: 500;
+      color: #AAAAAA;
+      margin-left: 10px;
+    }
+
+    .van-cell__label {
+      padding-top: 8px;
+    }
+
+    .van-field__control,
+    .van-stepper__input {
+      text-align: center;
+      color: #AAAAAA;
+    }
+  }
+
+  .partInput {
+    display: inline-block;
+    padding: 0;
+    width: 63px;
+    height: 36px;
+    line-height: 36px;
+    background: #F4F4F4;
+    border-radius: 6px;
+    text-align: center !important;
+  }
+
+  .partContent {
+    display: flex;
+    align-items: center;
+
+    span {
+      padding: 0 20px;
+      font-size: 16px;
+      font-weight: 500;
+      color: #AAAAAA;
+    }
+  }
+
+  .partBtn {
+    margin: 0 25px calc(20px + env(safe-area-inset-bottom));
+    width: calc(100% - 50px);
+    font-size: 18px;
+    font-weight: 500;
+  }
+}

+ 543 - 0
src/views/unit-test/unit-create/unit-edit-test/add-unit-item.tsx

@@ -0,0 +1,543 @@
+import { computed, defineComponent, onMounted, reactive } from 'vue'
+import styles from './add-unit-item.module.less'
+import {
+  Button,
+  Cell,
+  CellGroup,
+  Dialog,
+  Field,
+  Icon,
+  Popup,
+  Radio,
+  RadioGroup,
+  showToast,
+  Stepper
+} from 'vant'
+import iconDelete from '../image/icon-delete2.png'
+import iconMusic from '../image/icon-music.png'
+import iconRadioDefault from '../image/icon-radio-default.png'
+import iconReadioChecked from '../image/icon-radio-checked.png'
+import OHeader from '@/components/o-header'
+import OPopup from '@/components/o-popup'
+import MusicList from './music-list'
+import { difficultyCoefficients } from '.'
+import OActionSheet from '@/components/o-action-sheet'
+import requestOrigin from 'umi-request'
+import OSticky from '@/components/o-sticky'
+import { useRoute, useRouter } from 'vue-router'
+import { unitData } from './data'
+import request from '@/helpers/request'
+
+export default defineComponent({
+  name: 'add-unit-item',
+  setup() {
+    const route = useRoute()
+    const router = useRouter()
+    const state = reactive({
+      musicStatus: false,
+      // selectMusic: {} as any,
+      level: route.query.level,
+      dialogShow: false,
+      activeIndex: 0, // 选择的索引
+      activeRow: {} as any,
+      musicLevelShow: false,
+      actions: [] as any,
+      partShow: false,
+      checkedPart: null as any, // 选择小节类型
+      partLength: 0, // 小节长度
+      startPart: null as any, // 开始小节
+      endPart: null as any // 结束小节
+    })
+
+    const isEdit = computed(() => {
+      return route.query.musicId ? true : false
+    })
+
+    difficultyCoefficients.forEach((item: any) => {
+      state.actions.push({
+        name: item.label,
+        value: item.value
+      })
+    })
+
+    // 格式化难度数据
+    const filterDifficulty = (difficulty: string) => {
+      let str = ''
+      state.actions.forEach((action: any) => {
+        if (action.value === difficulty) {
+          str = action.name
+        }
+      })
+      return str
+    }
+
+    // 格式化小节数
+    const filterPart = (item: any) => {
+      if (item.partStart && item.partEnd) {
+        return item.partStart + '-' + item.partEnd + '小节'
+      } else {
+        return ''
+      }
+    }
+
+    const onSubmit = async () => {
+      let pass = true
+      for (let i = 0; i < forms.length; i++) {
+        if (!forms[i].musicId) {
+          pass = false
+          showToast('请选择曲目')
+          break
+        }
+        if (!forms[i].partName) {
+          pass = false
+          showToast('请选择小节')
+          break
+        }
+        if (!forms[i].difficulty) {
+          pass = false
+          showToast('请选择难度')
+          break
+        }
+        if (!forms[i].musicScore) {
+          pass = false
+          showToast('请输入曲目分数')
+          break
+        }
+
+        if (!forms[i].passScore) {
+          pass = false
+          showToast('请输入合格分数')
+          break
+        }
+      }
+
+      if (!pass) return
+      console.log(true, '1212')
+
+      if (isEdit.value) {
+        const questionList = unitData['level' + state.level]?.questionList
+        for (let i = 0; i < questionList.length; i++) {
+          if (questionList[i].question.mediaUrls === route.query.musicId) {
+            questionList[i].question = {
+              name: forms[0].musicName,
+              mediaUrls: forms[0].musicId,
+              questionTypeCode: 'PLAY',
+              totalScore: forms[0].musicScore,
+              questionExtendsInfo: JSON.stringify({
+                musicName: forms[0].musicName,
+                musicSheetId: forms[0].musicId,
+                start: forms[0].partStart,
+                end: forms[0].partEnd,
+                score: forms[0].passScore,
+                difficulty: forms[0].difficulty
+              })
+            }
+            break
+          }
+        }
+      } else {
+        const tempList: any = []
+        forms.forEach((item: any) => {
+          tempList.push({
+            unitExaminationId: null,
+            question: {
+              name: item.musicName,
+              mediaUrls: item.musicId,
+              questionTypeCode: 'PLAY',
+              totalScore: item.musicScore,
+              questionExtendsInfo: JSON.stringify({
+                musicName: item.musicName,
+                musicSheetId: item.musicId,
+                start: item.partStart,
+                end: item.partEnd,
+                score: item.passScore,
+                difficulty: item.difficulty
+              })
+            }
+          })
+        })
+        // 添加曲目
+        unitData['level' + state.level]?.questionList.push(...tempList)
+      }
+
+      router.back()
+    }
+
+    const _init = () => [
+      {
+        musicName: '',
+        musicId: null,
+        partName: '',
+        partLength: 0, // 总的小节数
+        partStart: null as any, // 开始小节数
+        partEnd: null as any, // 结束小节数
+        difficulty: null as any, // 难度
+        musicScore: null as any, // 曲目分数
+        passScore: null as any // 合格分数
+      }
+    ]
+    const forms = reactive(_init())
+
+    const getXMLPart = async (xmlFileUrl: string) => {
+      let xmlStatus = 'init'
+      // 第一个声部小节
+      let firstMeasures: any = null
+      let partLength = 0
+      try {
+        // 获取文件
+        const res = await requestOrigin.get(xmlFileUrl, {
+          mode: 'cors'
+        })
+        const xmlParse = new DOMParser().parseFromString(res, 'text/xml')
+        const parts = xmlParse.getElementsByTagName('part')
+        firstMeasures = parts[0]?.getElementsByTagName('measure')
+        xmlStatus = 'success'
+      } catch (error) {
+        xmlStatus = 'error'
+      }
+      // 判断读取小节数
+      if (xmlStatus == 'success') {
+        partLength = firstMeasures.length
+      }
+      return {
+        xmlStatus,
+        partLength
+      }
+    }
+
+    onMounted(async () => {
+      if (!route.query.musicId) return
+      const tData = unitData['level' + state.level]
+      let tempQ: any = null
+      tData?.questionList.forEach((item: any) => {
+        if (item.question.mediaUrls === route.query.musicId) {
+          tempQ = item
+        }
+      })
+      if (tempQ) {
+        const questionExtendsInfo = tempQ.question.questionExtendsInfo
+          ? JSON.parse(tempQ.question.questionExtendsInfo)
+          : {}
+
+        forms[0] = {
+          musicName: tempQ.question.name,
+          musicId: tempQ.question.mediaUrls,
+          partName: '',
+          partLength: 0, // 总的小节数
+          partStart: questionExtendsInfo.start || 0, // 开始小节数
+          partEnd: questionExtendsInfo.end || 0, // 结束小节数
+          difficulty: questionExtendsInfo.difficulty, // 难度
+          musicScore: tempQ.question.totalScore, // 曲目分数
+          passScore: questionExtendsInfo.score // 合格分数
+        }
+        // 初始化名称
+        forms[0].partName = filterPart(forms[0])
+
+        const { data } = await request.get('/api-teacher/musicSheet/detail/' + route.query.musicId)
+        const partData = await getXMLPart(data.xmlFileUrl)
+        forms[0].partLength = partData.partLength
+      }
+    })
+    return () => (
+      <div class={styles.addUnitItem}>
+        <OHeader />
+        {forms.map((item: any, index: number) => (
+          <CellGroup class={styles.cellGroup}>
+            <Cell center>
+              {{
+                title: () => (
+                  <div style={{ display: 'flex', 'align-items': 'center' }}>
+                    <Icon name={iconMusic} class={styles.iconMusic} />
+                    <div class={styles.title}>曲目{index + 1}</div>
+                  </div>
+                ),
+                value: () =>
+                  !isEdit.value && (
+                    <Icon
+                      name={iconDelete}
+                      class={[styles.iconDelete, forms.length <= 1 ? styles.disabled : '']}
+                      onClick={() => {
+                        state.dialogShow = true
+                        state.activeRow = {
+                          ...item,
+                          index
+                        }
+                      }}
+                    />
+                  )
+              }}
+            </Cell>
+            <Field
+              isLink
+              clearable={false}
+              inputAlign="right"
+              label="练习内容"
+              placeholder="请选择曲目"
+              autocomplete="off"
+              readonly
+              modelValue={item.musicName}
+              onClick={() => {
+                state.activeIndex = index
+                state.musicStatus = true
+                state.activeRow = item
+              }}
+            />
+            <Field
+              isLink
+              clearable={false}
+              inputAlign="right"
+              label="练习小节"
+              autocomplete="off"
+              readonly
+              modelValue={item.partName}
+              placeholder="请选择小节"
+              onClick={() => {
+                if (!item.musicId) {
+                  return showToast('请选择曲目')
+                }
+                state.activeRow = item
+                state.partShow = true
+                state.startPart = item.partStart
+                state.endPart = item.partEnd
+                state.partLength = item.partLength
+                if (item.partEnd === item.partLength) {
+                  state.checkedPart = '1'
+                } else {
+                  state.checkedPart = '2'
+                }
+              }}
+            />
+            <Field
+              isLink
+              clearable={false}
+              inputAlign="right"
+              label="练习难度"
+              autocomplete="off"
+              readonly
+              modelValue={filterDifficulty(item.difficulty)}
+              placeholder="请选择曲目难度"
+              onClick={() => {
+                state.musicLevelShow = true
+                state.activeRow = item
+                if (item.difficulty) {
+                  state.actions.forEach((action: any) => {
+                    if (action.value === item.difficulty) {
+                      action.selected = true
+                    } else {
+                      action.selected = false
+                    }
+                  })
+                } else {
+                  state.actions.forEach((action: any) => {
+                    action.selected = false
+                  })
+                }
+              }}
+            />
+            <Field
+              inputAlign="right"
+              label="曲目分数"
+              type="number"
+              autocomplete="off"
+              maxlength={3}
+              class={styles.inputControl}
+              v-model={item.musicScore}
+              center
+            >
+              {{
+                extra: () => (
+                  <div class={styles.loctionIconWrap}>
+                    <span> 分</span>
+                  </div>
+                )
+              }}
+            </Field>
+            <Field
+              inputAlign="right"
+              label="合格分数"
+              type="number"
+              maxlength={3}
+              autocomplete="off"
+              class={styles.inputControl}
+              v-model={item.passScore}
+              center
+            >
+              {{
+                extra: () => (
+                  <div class={styles.loctionIconWrap}>
+                    <span> 分</span>
+                  </div>
+                )
+              }}
+            </Field>
+          </CellGroup>
+        ))}
+
+        {!isEdit.value && (
+          <Button
+            round
+            class={styles.addBtn}
+            onClick={() => {
+              forms.push(..._init())
+            }}
+          >
+            <Icon name="plus" />
+            添加测验曲目
+          </Button>
+        )}
+
+        <OSticky position="bottom">
+          <div class={'btnGroup'}>
+            <Button type="primary" round block onClick={onSubmit}>
+              确认
+            </Button>
+          </div>
+        </OSticky>
+
+        <OPopup v-model:modelValue={state.musicStatus} style={{ background: '#F8F8F8' }}>
+          <MusicList
+            onConfirm={async (row: any) => {
+              // 判断是否选择一样的曲目
+              if (row.id === state.activeRow.musicId) {
+                state.musicStatus = false
+                return
+              }
+              state.activeRow.musicId = row.id
+              state.activeRow.musicName = row.musicSheetName
+              state.musicStatus = false
+
+              const partData = await getXMLPart(row.xmlFileUrl)
+              if (partData.xmlStatus === 'success') {
+                state.activeRow.partLength = partData.partLength
+                state.activeRow.partStart = 1
+                state.activeRow.partEnd = partData.partLength
+              } else {
+                state.activeRow.partLength = 0
+              }
+
+              state.activeRow.partName = ''
+            }}
+          />
+        </OPopup>
+
+        <Dialog
+          v-model:show={state.dialogShow}
+          showCancelButton
+          message={`请确认是否删除曲目${state.activeRow.index + 1}?`}
+          confirmButtonText="删除"
+          onConfirm={() => {
+            forms.splice(state.activeRow.index, 1)
+          }}
+        >
+          {{ title: () => <div class={styles.dialogDelete}>删除题目</div> }}
+        </Dialog>
+
+        {/* 曲目难度 */}
+        <OActionSheet
+          v-model:show={state.musicLevelShow}
+          actions={state.actions}
+          onSelect={(val: any) => {
+            state.actions.forEach((child: any) => {
+              child.selected = false
+            })
+            val.selected = true
+            state.activeRow.difficulty = val.value
+            state.musicLevelShow = false
+          }}
+        />
+
+        {/* 选择小节 */}
+        <Popup
+          round
+          position="bottom"
+          v-model:show={state.partShow}
+          closeable
+          class={styles.partPopup}
+        >
+          <div class={styles.partContainer}>
+            <div class={styles.partTitle}>请选择练习小节</div>
+
+            <RadioGroup v-model={state.checkedPart}>
+              <Cell
+                title={'全部小节'}
+                onClick={() => (state.checkedPart = '1')}
+                class={state.checkedPart == '1' && styles.activeCell}
+              >
+                {{
+                  icon: () => (
+                    <Radio name="1">
+                      {{
+                        icon: (props: any) => (
+                          <img src={props.checked ? iconReadioChecked : iconRadioDefault} />
+                        )
+                      }}
+                    </Radio>
+                  )
+                }}
+              </Cell>
+              <Cell
+                title={'部分小节'}
+                onClick={() => (state.checkedPart = '2')}
+                class={state.checkedPart == '2' && styles.activeCell}
+              >
+                {{
+                  icon: () => (
+                    <Radio name="2">
+                      {{
+                        icon: (props: any) => (
+                          <img src={props.checked ? iconReadioChecked : iconRadioDefault} />
+                        )
+                      }}
+                    </Radio>
+                  ),
+                  label: () => (
+                    <div class={styles.partContent}>
+                      <Stepper
+                        v-model={state.startPart}
+                        min={1}
+                        max={state.partLength}
+                        class={styles.partInput}
+                        showMinus={false}
+                        showPlus={false}
+                      ></Stepper>
+                      <span>至</span>
+                      <Stepper
+                        v-model={state.endPart}
+                        min={state.startPart}
+                        max={state.partLength}
+                        class={styles.partInput}
+                        showMinus={false}
+                        showPlus={false}
+                      ></Stepper>
+                      <span>小节</span>
+                    </div>
+                  )
+                }}
+              </Cell>
+            </RadioGroup>
+
+            <Button
+              round
+              block
+              type="primary"
+              class={styles.partBtn}
+              onClick={() => {
+                if (state.checkedPart === '1') {
+                  state.activeRow.partStart = 1
+                  state.activeRow.partEnd = state.activeRow.partLength
+                } else {
+                  state.activeRow.partStart = state.startPart
+                  state.activeRow.partEnd = state.endPart
+                }
+                state.activeRow.partName = filterPart(state.activeRow)
+                state.partShow = false
+              }}
+            >
+              确认
+            </Button>
+          </div>
+        </Popup>
+      </div>
+    )
+  }
+})

+ 40 - 0
src/views/unit-test/unit-create/unit-edit-test/data.ts

@@ -0,0 +1,40 @@
+import { reactive } from 'vue'
+
+const original = () => {
+  return {
+    courseType: '', // 班级类型
+    level: 'one' as any, // 选择的是哪个级别的tab
+    expiryDate: null as any, // 截止时间
+    level1: {
+      level: null as any, // 单团,双团,多团
+      totalScore: 0, // 总分,
+      questionNum: 0, // 题目数
+      passScore: 0, // 合格分数
+      timeMinutes: 0, // 测验时长
+      questionList: [] // 题目详情
+    },
+    level2: {
+      level: null as any, // 单团,双团,多团
+      totalScore: 0, // 总分,
+      questionNum: 0, // 题目数
+      passScore: 0, // 合格分数
+      timeMinutes: 0, // 测验时长
+      questionList: [] // 题目详情
+    },
+    level3: {
+      level: null as any, // 单团,双团,多团
+      totalScore: 0, // 总分,
+      questionNum: 0, // 题目数
+      passScore: 0, // 合格分数
+      timeMinutes: 0, // 测验时长
+      questionList: [] // 题目详情
+    }
+  }
+}
+
+export const unitData = reactive(original())
+
+// 重置对象
+export const resestState = () => {
+  Object.assign(unitData, original())
+}

+ 254 - 0
src/views/unit-test/unit-create/unit-edit-test/index.module.less

@@ -0,0 +1,254 @@
+.unitTest {
+  overflow: hidden;
+  min-height: 100vh;
+
+  :global {
+    .van-dialog__message {
+      font-size: 16px;
+      color: #333;
+      padding-top: 18px
+    }
+  }
+}
+
+.itemWrap {
+  margin: 12px 13px;
+  background: #ffffff;
+  border-radius: 10px;
+  padding-bottom: 26px;
+
+  .titleWrap {
+    padding: 0 12px;
+    height: 22px;
+    font-size: 16px;
+    font-weight: 500;
+    color: #333333;
+    line-height: 22px;
+  }
+
+  .itemBottom {
+    margin-top: 15px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-around;
+    text-align: center;
+    padding: 0 12px;
+
+    .itemBottomDot {
+      flex-basis: 33.33%;
+      position: relative;
+
+      .dotMain {
+        font-size: 20px;
+        font-family: DINAlternate, DINAlternate;
+        font-weight: bold;
+        color: #333333;
+        line-height: 30px;
+        margin-bottom: 4px;
+
+        span {
+          margin-left: 1px;
+          font-size: 12px;
+          font-weight: 400;
+          color: #333333;
+          line-height: 17px;
+        }
+      }
+
+      .dotSub {
+        font-size: 12px;
+        font-weight: 400;
+        color: #777777;
+        line-height: 17px;
+      }
+    }
+  }
+
+  .uniTimeWrap {
+    padding: 12px;
+    margin-bottom: 20px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    border-bottom: 1px solid #f2f2f2;
+
+    .uniTimeWrapLeft {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      font-size: 16px;
+      font-weight: 600;
+      line-height: 20px;
+
+      .clockIcon {
+        margin-top: -2px;
+        font-size: 18px;
+        margin-right: 4px;
+      }
+    }
+  }
+}
+
+.editBtn {
+  color: #F67146;
+  background-color: #FFF5F2;
+  border-color: #FFB39B;
+
+  font-size: 13px;
+  height: 26px;
+  padding-top: 2px;
+  line-height: 25px;
+}
+
+.tableSection {
+  background: #FFFFFF;
+  border-radius: 10px;
+  margin: 0 13px 13px;
+  overflow: hidden;
+
+  :global {
+    .van-col {
+      line-height: 24px;
+    }
+
+    .van-col--6,
+    .van-col--4 {
+      text-align: center;
+    }
+  }
+
+  .title {
+    font-size: 14px;
+    color: #777777;
+    padding: 13px 12px;
+  }
+
+  .content {
+    padding: 13px 12px;
+    font-weight: 500;
+    color: #333333;
+    font-size: 13px;
+  }
+
+  .btnGroup {
+    display: flex;
+    align-items: center;
+  }
+
+  .icon {
+    font-size: 24px;
+
+    &:last-child {
+      margin-left: 8px;
+    }
+  }
+}
+
+.addBtn {
+  display: block;
+  padding: 0 48px;
+  font-size: 15px;
+  font-family: PingFangSC, PingFang SC;
+  font-weight: 500;
+  color: #F67146;
+  background: #FFF5F2;
+  border-radius: 20px;
+  border: 1px solid #FFB39B;
+  height: 39px;
+  margin: 15px auto;
+
+  :global {
+    .van-icon {
+      font-size: 16px;
+      font-weight: bold;
+      margin-left: 6px;
+    }
+  }
+}
+
+.dialogDelete {
+  display: flex;
+  align-items: center;
+  padding-left: var(--van-dialog-message-padding);
+
+  &::before {
+    content: '';
+    display: inline-block;
+    width: 4px;
+    height: 14px;
+    background: #FF8057;
+    border-radius: 2px;
+    margin-right: 6px;
+  }
+}
+
+.partContainer {
+  .partTitle {
+    font-size: 18px;
+    font-weight: 500;
+    color: #131415;
+    line-height: 25px;
+    text-align: center;
+    padding: 20px 0;
+  }
+
+
+  .inputControl {
+    :global {
+      .van-field__body {
+        justify-content: flex-end;
+        margin-right: 8px;
+      }
+
+      .van-field__control {
+        width: 58px;
+        height: 33px;
+        background: #F4F4F4;
+        border-radius: 8px;
+        text-align: center;
+      }
+    }
+  }
+
+  :global {
+    .van-cell {
+      padding: 20px 15px;
+    }
+
+    .van-cell__title {
+      font-size: 16px;
+      font-weight: 500;
+      color: #777;
+      margin-left: 10px;
+    }
+
+    .van-cell__label {
+      padding-top: 8px;
+    }
+
+    .van-field__control {
+      text-align: center;
+    }
+
+    .van-stepper {
+      width: 100%;
+    }
+  }
+
+  .partBtn {
+    margin: 0 25px calc(20px + env(safe-area-inset-bottom));
+    width: calc(100% - 50px);
+    font-size: 18px;
+    font-weight: 500;
+  }
+}
+
+.settingStatus {
+  :global {
+    .van-popup__close-icon--top-right {
+      top: 22px;
+    }
+  }
+}

+ 306 - 0
src/views/unit-test/unit-create/unit-edit-test/index.tsx

@@ -0,0 +1,306 @@
+import { computed, defineComponent, onMounted, reactive, watch } from 'vue'
+import styles from './index.module.less'
+import { Button, CellGroup, Col, Dialog, Field, Icon, Popup, Row, showToast, Stepper } from 'vant'
+import iconMenu from '../image/icon-menu.png'
+import iconDelte from '../image/icon-delete.png'
+import iconEdit from '../image/icon-edit.png'
+import request from '@/helpers/request'
+import { useRoute, useRouter } from 'vue-router'
+import OHeader from '@/components/o-header'
+import { unitData } from './data'
+
+//题目类型
+export const questionTypeCode: { [_: string]: any } = {
+  RADIO: '单选题',
+  CHECKBOX: '多选题',
+  PLAY: '演奏题',
+  SORT: '排序题',
+  LINK: '连连看'
+}
+//难度
+// { label: '普通级', value: '1' },
+//                   { label: '进阶级', value: '2' },
+//                   { label: '大师级', value: '3' }
+// ONE('初级'), TWO('中级'), THREE('高级')
+export const difficultyCoefficients: any = [
+  { label: '普通级', value: '1' },
+  { label: '进阶级', value: '2' },
+  { label: '大师级', value: '3' }
+]
+
+export default defineComponent({
+  name: 'unit-edit-test',
+  setup() {
+    const router = useRouter()
+    const route = useRoute()
+
+    const state = reactive({
+      unitExaminationName: route.query.unitExaminationName,
+      questionList: [] as any, // 系统自带题目
+      level: route.query.level ? Number(route.query.level) : (null as any),
+      dialogShow: false,
+      activeRow: {} as any,
+      settingStatus: false,
+      pScore: null,
+      pTime: null
+    })
+
+    const wTotalScore = computed(() => {
+      const sQuestionList = state.questionList
+      const aQuestionList = unitData['level' + state.level]?.questionList || []
+      let countScore = 0
+      sQuestionList.forEach((s: any) => {
+        countScore += Number(s.question.totalScore) || 0
+      })
+
+      aQuestionList.forEach((s: any) => {
+        countScore += Number(s.question.totalScore) || 0
+      })
+      unitData['level' + state.level].totalScore = countScore
+      return countScore
+    })
+
+    const wQuestionNum = computed(() => {
+      const sQuestionList = state.questionList
+      const aQuestionList = unitData['level' + state.level]?.questionList || []
+      unitData['level' + state.level].questionNum = sQuestionList.length + aQuestionList.length
+      return sQuestionList.length + aQuestionList.length
+    })
+
+    // 初始化数据
+    const __init = () => {
+      const query = route.query
+      // if (unitData['level' + state.level]?.questionList.length <= 0) {
+      unitData['level' + state.level].totalScore = query.totalScore
+      unitData['level' + state.level].questionNum = query.questionNum
+      if (unitData['level' + state.level].passScore <= 0) {
+        unitData['level' + state.level].passScore = query.totalScore
+      }
+      if (unitData['level' + state.level].timeMinutes <= 0) {
+        unitData['level' + state.level].timeMinutes = query.timeMinutes
+      }
+      unitData['level' + state.level].level = query.level
+      // }
+    }
+
+    const getDetail = async () => {
+      try {
+        const { data } = await request.get(
+          '/api-teacher/unitExaminationDetail/detail/' + route.query.unitExaminationId
+        )
+        state.questionList = data || []
+      } catch {
+        //
+      }
+    }
+
+    const onDelete = (row: any) => {
+      state.activeRow = row
+      state.dialogShow = true
+    }
+
+    const onEdit = (row: any) => {
+      router.push({
+        path: '/add-unit-item',
+        query: {
+          level: state.level,
+          musicId: row.question.mediaUrls
+        }
+      })
+    }
+
+    onMounted(() => {
+      __init()
+      getDetail()
+    })
+    return () => (
+      <div class={styles.unitTest}>
+        <OHeader />
+        <div class={styles.itemWrap}>
+          <div class={styles.uniTimeWrap}>
+            <div class={styles.uniTimeWrapLeft}>
+              <Icon name={iconMenu} class={styles.clockIcon}></Icon> {state.unitExaminationName}
+            </div>
+
+            <Button
+              plain
+              round
+              class={styles.editBtn}
+              onClick={() => {
+                state.settingStatus = true
+                state.pScore = unitData['level' + state.level]?.passScore
+                state.pTime = unitData['level' + state.level]?.timeMinutes
+              }}
+            >
+              修改
+            </Button>
+          </div>
+          <div class={styles.itemBottom}>
+            <div class={[styles.itemBottomDot, 'gridBorderRight']}>
+              <p class={styles.dotMain} style={{ color: '#999999' }}>
+                {wTotalScore.value || 0}
+              </p>
+              <p class={styles.dotSub}> 总分</p>
+            </div>
+            <div class={[styles.itemBottomDot, 'gridBorderRight']}>
+              <p class={styles.dotMain} style={{ color: '#999999' }}>
+                {wQuestionNum.value || 0}
+              </p>
+              <p class={styles.dotSub}>题目数 </p>
+            </div>
+            <div class={[styles.itemBottomDot, 'gridBorderRight']}>
+              <p class={styles.dotMain}>{unitData['level' + state.level]?.passScore || 0}</p>
+              <p class={styles.dotSub}>合格分 </p>
+            </div>
+            <div class={styles.itemBottomDot}>
+              <p class={styles.dotMain}>{unitData['level' + state.level]?.timeMinutes || 0}</p>
+              <p class={styles.dotSub}>测验时长/min </p>
+            </div>
+          </div>
+        </div>
+
+        <div class={styles.tableSection}>
+          <Row class={[styles.title, 'van-hairline--bottom']}>
+            <Col span={10}>题目名称</Col>
+            <Col span={6}>题目类型</Col>
+            <Col span={4}>分值</Col>
+            <Col span={4}>操作</Col>
+          </Row>
+          {state.questionList.map((q: any) => (
+            <Row class={[styles.content, 'van-hairline--bottom']}>
+              <Col span={10} class="van-ellipsis">
+                {q.question.name}
+              </Col>
+              <Col span={6}>{questionTypeCode[q.question.questionTypeCode]}</Col>
+              <Col span={4}>{q.question.totalScore}</Col>
+              <Col span={4}>--</Col>
+            </Row>
+          ))}
+          {/* 新添加的曲目 */}
+          {unitData['level' + state.level]?.questionList.map((q: any, index: number) => (
+            <Row class={[styles.content, 'van-hairline--bottom']}>
+              <Col span={10} class="van-ellipsis">
+                {q.question.name}
+              </Col>
+              <Col span={6}>{questionTypeCode[q.question.questionTypeCode]}</Col>
+              <Col span={4}>{q.question.totalScore}</Col>
+              <Col span={4}>
+                <div class={styles.btnGroup}>
+                  <Icon name={iconEdit} class={styles.icon} onClick={() => onEdit(q)} />
+                  <Icon
+                    name={iconDelte}
+                    class={styles.icon}
+                    onClick={() => onDelete({ ...q, index })}
+                  />
+                </div>
+              </Col>
+            </Row>
+          ))}
+
+          <Button
+            round
+            class={styles.addBtn}
+            onClick={() => {
+              router.push('/add-unit-item?level=' + state.level)
+            }}
+          >
+            <Icon name="plus" />
+            添加测验曲目
+          </Button>
+        </div>
+
+        <Dialog
+          v-model:show={state.dialogShow}
+          showCancelButton
+          message={`请确认是否删除《${state.activeRow?.question?.name}》?`}
+          messageAlign="left"
+          confirmButtonText="删除"
+          onConfirm={() => {
+            unitData['level' + state.level]?.questionList.splice(state.activeRow.index, 1)
+          }}
+        >
+          {{ title: () => <div class={styles.dialogDelete}>删除题目</div> }}
+        </Dialog>
+
+        <Popup
+          position="bottom"
+          round
+          v-model:show={state.settingStatus}
+          closeable
+          class={styles.settingStatus}
+        >
+          <div class={styles.partContainer}>
+            <div class={styles.partTitle}>修改</div>
+            <CellGroup class={styles.cellGroup} border={false}>
+              <Field
+                inputAlign="right"
+                label="合格分"
+                type="number"
+                autocomplete="off"
+                maxlength={3}
+                class={styles.inputControl}
+                v-model={state.pScore}
+                center
+              >
+                {{
+                  input: () => (
+                    <Stepper
+                      v-model={state.pScore}
+                      max={wTotalScore.value}
+                      showMinus={false}
+                      showPlus={false}
+                    ></Stepper>
+                  ),
+                  extra: () => (
+                    <div class={styles.loctionIconWrap}>
+                      <span>
+                        分<i style={{ color: '#fff' }}>钟</i>
+                      </span>
+                    </div>
+                  )
+                }}
+              </Field>
+              <Field
+                inputAlign="right"
+                label="测验时长"
+                type="number"
+                maxlength={3}
+                autocomplete="off"
+                class={styles.inputControl}
+                center
+              >
+                {{
+                  input: () => (
+                    <Stepper v-model={state.pTime} showMinus={false} showPlus={false}></Stepper>
+                  ),
+                  extra: () => (
+                    <div class={styles.loctionIconWrap}>
+                      <span> 分钟</span>
+                    </div>
+                  )
+                }}
+              </Field>
+            </CellGroup>
+            <Button
+              round
+              block
+              type="primary"
+              class={styles.partBtn}
+              onClick={() => {
+                if (!state.pScore) return showToast('请输入合格分')
+                if (!state.pTime) return showToast('请输入测试时长')
+
+                unitData['level' + state.level].passScore = state.pScore
+                unitData['level' + state.level].timeMinutes = state.pTime
+
+                state.settingStatus = false
+              }}
+            >
+              确认
+            </Button>
+          </div>
+        </Popup>
+      </div>
+    )
+  }
+})

+ 154 - 0
src/views/unit-test/unit-create/unit-edit-test/music-list.module.less

@@ -0,0 +1,154 @@
+.accompanyCategory {
+  box-sizing: border-box;
+
+  div {
+    box-sizing: border-box;
+  }
+
+  .container {
+    margin: 12px;
+    height: 140px;
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-evenly;
+    align-items: flex-start;
+  }
+
+  .title {
+    font-size: 20px;
+    font-weight: 600;
+    color: #ffffff;
+    text-shadow: 0px 2px 4px #7b63ff;
+  }
+
+  .sub {
+    font-size: 14px;
+    font-weight: 500;
+    color: #ffffff;
+    text-shadow: 0px 2px 4px #8771ff;
+  }
+
+  .btn {
+    width: 90px;
+    height: 34px;
+    line-height: 28px;
+    padding-left: 20px;
+    background-repeat: no-repeat;
+    background-size: 100% 110%;
+    margin-left: -10px;
+    font-size: 18px;
+    font-weight: 500;
+  }
+}
+
+.accompany-music-list {
+  :global {
+    .van-pull-refresh {
+      min-height: 100vh;
+    }
+  }
+
+  div {
+    box-sizing: border-box;
+  }
+
+  .heade {
+    height: calc(var(--van-dropdown-menu-height) + 1.6rem);
+
+    :global {
+      .van-dropdown-menu__bar {
+        box-shadow: none;
+      }
+
+      .van-dropdown-menu__title {
+        font-size: 14px;
+        font-weight: 400;
+        color: #333;
+        font-family: PingFangSC-Regular, PingFang SC;
+      }
+
+      .van-dropdown-menu__title:after {
+        right: -6px;
+        margin-top: -5px;
+        border-width: 3.5px;
+        border-color: transparent transparent #aaaaaa #aaaaaa;
+        border-radius: 1.2px;
+        opacity: 0.8;
+        content: '';
+      }
+
+      .van-dropdown-menu__title--active:after {
+        margin-top: -1px;
+        border-color: transparent transparent currentColor currentColor;
+      }
+    }
+  }
+
+  .filter {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 60px;
+    background-color: #f8f8f8;
+
+    .filterBox {
+      display: flex;
+      width: 100%;
+    }
+
+    :global {
+      .van-search {
+        display: flex;
+        align-items: center;
+        flex: 1;
+        margin: 0 12px;
+        padding: 0;
+        height: 40px;
+        background: #fff;
+        border-radius: 20px !important;
+
+        .van-search__content {
+          background: transparent !important;
+        }
+      }
+
+      .van-dropdown-menu,
+      .van-dropdown-menu__bar {
+        height: 100%;
+        background: transparent;
+      }
+
+      .van-search__field {
+        background: transparent;
+        border-radius: 20px 0 0 20px;
+        padding-left: 20px;
+        height: 36px;
+      }
+
+      .van-search__action {
+        border-radius: 0 20px 20px 0;
+        background-color: #fff;
+        height: 36px;
+        display: flex;
+        align-items: center;
+      }
+    }
+
+    .searchBtn {
+      width: 56px;
+      height: 27px;
+      line-height: 27px;
+      border-radius: 20px;
+      background-color: var(--van-primary);
+      font-size: 14px;
+      text-align: center;
+      color: #fff;
+
+      &:active {
+        opacity: 0.8;
+      }
+    }
+  }
+}

+ 302 - 0
src/views/unit-test/unit-create/unit-edit-test/music-list.tsx

@@ -0,0 +1,302 @@
+import OEmpty from '@/components/o-empty'
+import request from '@/helpers/request'
+import { state } from '@/state'
+import OFullRefresh from '@/components/o-full-refresh'
+import { Cell, CellGroup, DropdownItem, DropdownMenu, Icon, List } from 'vant'
+import { defineComponent, reactive, ref, onMounted, nextTick, computed } from 'vue'
+import { useRoute } from 'vue-router'
+import styles from './index.module.less'
+import OSticky from '@/components/o-sticky'
+import OSearch from '@/components/o-search'
+import OHeader from '@/components/o-header'
+import { getImage } from '@/views/accompany/images'
+import { unitData } from './data'
+
+export default defineComponent({
+  name: 'accompany-music-list',
+  emits: ['confirm'],
+  setup(props, { emit }) {
+    const imgDefault = getImage('icon-music.svg')
+    const subjectKey = state.user?.data?.phone || 'accompany-music-list-subject'
+    const subjectId =
+      localStorage.getItem(subjectKey) || state.user?.data?.subjectId?.split(',')?.[0] || ''
+    const data = reactive({
+      loading: false,
+      firstRender: false,
+      finished: false,
+      refreshing: false,
+      musicTree: [] as any,
+      pagenation: {
+        page: 1,
+        rows: 20
+      },
+      value0: null,
+      value1: null,
+      value2: null,
+      PopoverOpen: false,
+      list: [] as any,
+      keyword: '',
+      // 合奏班默认选中所有声部
+      musicSubject: unitData.courseType === 'INSTRUMENTAL_ENSEMBLE' ? '' : subjectId,
+      subjectList: [] as any
+    })
+
+    const getTree = async () => {
+      try {
+        const res: any = await request.get(
+          state.platformApi + '/musicSheetCategories/queryTree?enable=true'
+        )
+        if (Array.isArray(res?.data)) {
+          data.musicTree = res.data
+
+          // 默认选择独奏曲目
+          const item = data.musicTree.find((item: any) => item.name === '独奏曲目')
+          if (item) {
+            data.value0 = item.id
+          }
+        }
+      } catch (error) {
+        console.log(error)
+      }
+    }
+    // 获取声部信息
+    const getSubjects = async () => {
+      try {
+        const subjects = await request.post(state.platformApi + '/open/subjectBasicConfig/page', {
+          data: {
+            enableFlag: true,
+            page: 1,
+            rows: 100
+          }
+        })
+        const rows = subjects.data.rows || []
+        data.subjectList.push({
+          text: '全部',
+          value: ''
+        })
+        rows.forEach((item: any) => {
+          data.subjectList.push({
+            text: item.subjectName,
+            value: item.subjectId + ''
+          })
+        })
+      } catch {
+        //
+      }
+    }
+    const option0 = computed(() => {
+      const v1: any = data.musicTree
+      if (Array.isArray(v1)) {
+        const list = v1.map((m: any) => {
+          if (!data.value0) {
+            data.value0 = m.id
+            data.value1 = null
+            data.value2 = null
+          }
+          return {
+            text: m.name,
+            value: m.id
+          }
+        })
+        return list
+      }
+      return []
+    })
+
+    const option1 = computed(() => {
+      const v1: any = data.musicTree.find((n: any) => n.id == data.value0)
+      if (Array.isArray(v1?.musicSheetCategoriesList)) {
+        const list = v1.musicSheetCategoriesList.map((m: any) => {
+          if (!data.value1) {
+            data.value1 = m.id
+            data.value2 = null
+          }
+          return {
+            text: m.name,
+            value: m.id
+          }
+        })
+        return list
+      }
+      return []
+    })
+    const option2 = computed(() => {
+      const v1: any = data.musicTree.find((n: any) => n.id == data.value0)
+      if (Array.isArray(v1?.musicSheetCategoriesList)) {
+        const v2: any = v1.musicSheetCategoriesList.find((n: any) => n.id == data.value1)
+        if (Array.isArray(v2?.musicSheetCategoriesList)) {
+          const list = [{ text: '全部', value: null }].concat(
+            v2.musicSheetCategoriesList.map((m: any) => {
+              return {
+                text: m.name,
+                value: m.id
+              }
+            })
+          )
+          return list
+        }
+      }
+      return [{ text: '全部', value: null }]
+    })
+
+    const getList = async () => {
+      if (data.loading) return
+      data.loading = true
+      const bodyData: any = {
+        ...data.pagenation,
+        keyword: data.keyword,
+        musicSheetCategoriesId: data.value2 || data.value1,
+        status: 1,
+        musicSubject: data.musicSubject
+      }
+      try {
+        const res: any = await request.post(state.platformApi + '/musicSheet/page', {
+          data: bodyData,
+          hideLoading: true
+        })
+        if (Array.isArray(res?.data?.rows)) {
+          data.list = [].concat(data.list, res.data.rows)
+          data.pagenation.page += 1
+          data.finished = res.data.rows.length < data.pagenation.rows ? true : false
+        } else {
+          data.finished = true
+        }
+      } catch (error) {
+        data.finished = true
+      }
+      data.loading = false
+      data.refreshing = false
+      data.firstRender = true
+    }
+    // 重置搜索
+    const onSearch = () => {
+      data.pagenation.page = 1
+      data.list = []
+      data.finished = false
+      data.list = []
+      getList()
+    }
+
+    //
+    const onDetail = (row: any) => {
+      emit('confirm', row)
+    }
+    onMounted(async () => {
+      await getTree()
+      nextTick(async () => {
+        await getSubjects()
+        await getList()
+      })
+    })
+
+    return () => (
+      <div class={styles['accompany-music-list']}>
+        <OSticky
+          mode="sticky"
+          class={styles.heade}
+          onGetHeight={(height: number) => {
+            document.documentElement.style.setProperty('--header-height', height + 'px')
+          }}
+        >
+          <OHeader border={false} />
+          <div>
+            <DropdownMenu activeColor="var(--van-primary)">
+              <DropdownItem
+                v-model:modelValue={data.musicSubject}
+                options={data.subjectList}
+                onChange={() => {
+                  localStorage.setItem(subjectKey, data.musicSubject)
+                  onSearch()
+                }}
+              ></DropdownItem>
+              <DropdownItem
+                v-model:modelValue={data.value0}
+                options={option0.value}
+                onChange={() => {
+                  data.value1 = null
+                  data.value2 = null
+                  nextTick(() => {
+                    onSearch()
+                  })
+                }}
+              ></DropdownItem>
+              <DropdownItem
+                v-model:modelValue={data.value1}
+                options={option1.value}
+                onChange={() => {
+                  data.value2 = null
+                  onSearch()
+                }}
+              ></DropdownItem>
+              <DropdownItem
+                v-model:modelValue={data.value2}
+                options={option2.value as any}
+                onChange={() => onSearch()}
+              ></DropdownItem>
+            </DropdownMenu>
+            <div class={styles.filter}>
+              <OSearch
+                background="#f8f8f8"
+                inputBackground="white"
+                class={styles.filterBox}
+                onSearch={(keyword: string) => {
+                  data.keyword = keyword
+                  onSearch()
+                }}
+              />
+            </div>
+          </div>
+        </OSticky>
+        <OFullRefresh
+          v-model:modelValue={data.refreshing}
+          onRefresh={onSearch}
+          style="min-height: calc(100vh - var(--header-height))"
+        >
+          <List
+            loading-text=" "
+            immediateCheck={false}
+            loading={data.loading}
+            v-model:finished={data.finished}
+            finishedText=" "
+            onLoad={() => {
+              getList()
+            }}
+          >
+            <CellGroup inset>
+              {data.list.map((item: any) => {
+                return (
+                  <Cell
+                    size="large"
+                    center
+                    title={item.musicSheetName}
+                    clickable
+                    onClick={() => onDetail(item)}
+                  >
+                    {{
+                      icon: () => (
+                        <Icon style={{ marginRight: '12px' }} size={40} name={imgDefault} />
+                      )
+                    }}
+                  </Cell>
+                )
+              })}
+            </CellGroup>
+            <div style={{ height: '40px' }}></div>
+          </List>
+          {data.firstRender && !data.loading && !data.list.length && <OEmpty tips="暂无曲谱" />}
+        </OFullRefresh>
+        {/* <Popup teleport="body" position="bottom" round v-model:show={staffData.open}>
+          <Picker
+            columns={staffData.musicXml[staffData.instrumentName]}
+            onConfirm={(value) => {
+              staffData.open = false
+              staffData.partIndex = value.selectedValues[0]
+              openView({ id: staffData.instrumentName })
+            }}
+            onCancel={() => (staffData.open = false)}
+          />
+        </Popup> */}
+      </div>
+    )
+  }
+})

+ 3 - 4
src/views/unit-test/unit-detail/index.tsx

@@ -49,7 +49,6 @@ export default defineComponent({
         // 正确答案
         state.answerResult = answerResult ? JSON.parse(answerResult) : []
 
-        console.log(state.answerResult, temp)
         temp.forEach((item: any) => {
           item.userAnswer = formatUserAnswers(item, studentAnswerJson)
           item.showAnalysis = true
@@ -117,7 +116,7 @@ export default defineComponent({
         // 问题列表
         state.questionList = temp
         // 正确答案
-        console.log(state.questionList, 'state.questionList')
+        // console.log(state.questionList, 'state.questionList')
       } catch {
         //
       }
@@ -184,7 +183,7 @@ export default defineComponent({
           let status = true
           // console.log(allImg)
           allImg.forEach((img: any) => {
-            console.log(img.complete)
+            // console.log(img.complete)
             if (!img.complete) {
               status = false
             }
@@ -201,7 +200,7 @@ export default defineComponent({
           }
           const rect = useRect(currentItemDom)
           // console.log('🚀 ~ setTimeout ~ currentItemDom', currentItemDom)
-          console.log('🚀 ~ setTimeout ~ rect', rect, state.currentIndex)
+          // console.log('🚀 ~ setTimeout ~ rect', rect, state.currentIndex)
 
           state.swipeHeight = rect.height
         }, 100)

+ 1 - 1
src/views/unit-test/unit-list/index.tsx

@@ -71,7 +71,7 @@ export default defineComponent({
 
         form.params.page = res.data.current + 1
         //form.list  =form.list .concat(res.data.rows || [])
-        form.list = res.data.rows
+        form.list = form.list.concat(res.data.rows || [])
         form.listState.dataShow = form.list.length > 0
         loading.value = false
         // form.listState.finished = true

+ 1 - 1
vite.config.ts

@@ -13,7 +13,7 @@ function resolve(dir: string) {
 // https://github.com/vitejs/vite/issues/1930 .env
 // const proxyUrl = 'https://online.lexiaoya.cn/'
 // const proxyUrl = 'https://test.lexiaoya.cn/'
-const proxyUrl = 'https://dev.lexiaoya.cn/'
+const proxyUrl = 'https://test.lexiaoya.cn/'
 // const proxyUrl = 'http://47.98.131.38:8989/'
 // const proxyUrl = 'http://192.168.3.20:8989/' // 邹旋
 // const proxyUrl = 'http://192.168.3.143:8989/' // 尚科