Browse Source

Merge branch 'iteration-orchestra-new' into jenkins-main

lex 1 year ago
parent
commit
f59161e8ae

+ 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 选择阶段自测
     ]

BIN
src/views/unit-test/unit-create/image/icon-delete.png


BIN
src/views/unit-test/unit-create/image/icon-delete2.png


BIN
src/views/unit-test/unit-create/image/icon-edit.png


BIN
src/views/unit-test/unit-create/image/icon-menu.png


BIN
src/views/unit-test/unit-create/image/icon-music.png


BIN
src/views/unit-test/unit-create/image/icon-radio-checked.png


BIN
src/views/unit-test/unit-create/image/icon-radio-default.png


+ 20 - 1
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,11 +21,13 @@
     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;
@@ -37,6 +41,7 @@
           line-height: 17px;
         }
       }
+
       .dotSub {
         font-size: 12px;
         font-weight: 400;
@@ -45,6 +50,7 @@
       }
     }
   }
+
   .uniTimeWrap {
     padding: 12px;
     margin-top: 20px;
@@ -53,12 +59,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 +75,14 @@
     }
   }
 }
+
+.editBtn {
+  color: #F67146;
+  background-color: #FFF5F2;
+  border-color: #FFB39B;
+
+  font-size: 13px;
+  height: 26px;
+  padding-top: 2px;
+  line-height: 25px;
+}

+ 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>
     )
   }
 })

+ 75 - 10
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',
@@ -108,7 +110,6 @@ export default defineComponent({
 
     onMounted(() => {
       forms.value = { ...JSON.parse(sessionStorage.getItem('unit-create') || '{}') } as any
-      console.log(forms.value)
       const query = route.query
       // api-teacher/lessonCoursewareExaminationMapper/detailByCourseId
       // 判断是从课程结束后,还是正常创建
@@ -147,20 +148,72 @@ export default defineComponent({
         return
       }
       try {
+        // 合并参数
+        //      "passScore": 48,
+        // "questionList": [
+        //     {
+        //         "musicSheetId": "539",
+        //         "musicName": "长笛教程1-2",
+        //         "questionTypeCode": "PLAY",
+        //         "level": 1,
+        //         "score": 30,
+        //         "totalScore": 10,
+        //         "difficulty": "ONE",
+        //         "start": "1",
+        //         "end": "5",
+        //         "mediaUrls": "539"
+        //     }
+        // ]
+        const params = {
+          classGroupId: forms.value.classGroupId,
+          lessonCoursewareExaminationMapperId: forms.value.testId,
+          unitExaminationName: `${forms.value?.coursewareName}-${forms.value?.testName}`,
+          expiryDate: forms.value.expiryDate,
+          unitConfig: [] as any,
+          question: [] 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.question.push({
+              musicSheetId: unit.question.mediaUrls,
+              musicName: unit.question.name,
+              questionTypeCode: 'PLAY',
+              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 +227,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}
@@ -241,10 +298,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>

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

@@ -0,0 +1,198 @@
+.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 {
+    :global {
+      .van-cell__title {
+        color: #F67146;
+      }
+    }
+  }
+
+  :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 {
+      text-align: center;
+    }
+  }
+
+  .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;
+  }
+}

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

@@ -0,0 +1,536 @@
+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
+} 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, // 选择小节类型
+      startPart: null, // 开始小节
+      endPart: null // 结束小节
+    })
+
+    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
+                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}>
+                      <Field
+                        type="number"
+                        maxlength={3}
+                        class={styles.partInput}
+                        v-model={state.startPart}
+                      />
+                      <span>至</span>
+                      <Field
+                        type="number"
+                        maxlength={3}
+                        class={styles.partInput}
+                        v-model={state.endPart}
+                      />
+                      <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>
+    )
+  }
+})

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

@@ -0,0 +1,37 @@
+import { reactive } from 'vue'
+
+const original = () => {
+  return {
+    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())
+}

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

@@ -0,0 +1,241 @@
+.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: 24px;
+        color: #333333;
+        line-height: 30px;
+        margin-bottom: 4px;
+        font-family: 'DINA' !important;
+
+        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;
+    }
+  }
+
+  .partBtn {
+    margin: 0 25px calc(20px + env(safe-area-inset-bottom));
+    width: calc(100% - 50px);
+    font-size: 18px;
+    font-weight: 500;
+  }
+}

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

@@ -0,0 +1,285 @@
+import { computed, defineComponent, onMounted, reactive, watch } from 'vue'
+import styles from './index.module.less'
+import { Button, CellGroup, Col, Dialog, Field, Icon, Popup, Row, showToast } 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' }
+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
+      unitData['level' + state.level].passScore = query.totalScore
+      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>
+          <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
+              >
+                {{
+                  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}
+                v-model={state.pTime}
+                center
+              >
+                {{
+                  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;
+      }
+    }
+  }
+}

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

@@ -0,0 +1,288 @@
+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'
+
+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: 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
+        }
+        nextTick(() => {
+          getList()
+        })
+      } 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 || []
+        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(() => {
+      getSubjects()
+      getTree()
+    })
+
+    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
+                  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