Jelajahi Sumber

添加功能

lex 2 tahun lalu
induk
melakukan
15970b90bf
29 mengubah file dengan 1075 tambahan dan 137 penghapusan
  1. 16 0
      src/router/routes-common.ts
  2. 1 3
      src/school/approval-manage/batch-adjust.tsx
  3. 12 0
      src/school/approval-manage/course-adjust.tsx
  4. 1 1
      src/school/companion-teacher/unbind.tsx
  5. 5 4
      src/school/train-planning/component/course-preview/index.tsx
  6. 16 13
      src/school/train-planning/modal/timer/index.tsx
  7. TEMPAT SAMPAH
      src/student/download/images/manage_bg.png
  8. TEMPAT SAMPAH
      src/student/download/images/student_bg.png
  9. TEMPAT SAMPAH
      src/student/download/images/teacher_bg.png
  10. 22 3
      src/student/download/index.module.less
  11. 23 5
      src/student/download/index.tsx
  12. 8 4
      src/student/download/transfer.tsx
  13. 3 8
      src/student/music-group/pre-apply/component/apply.tsx
  14. 1 1
      src/student/music-group/shop-address/address-operation.tsx
  15. TEMPAT SAMPAH
      src/views/unit-test/images/icon-song.png
  16. 9 1
      src/views/unit-test/index.tsx
  17. 9 3
      src/views/unit-test/model/choice-question/index.module.less
  18. 5 0
      src/views/unit-test/model/choice-question/index.tsx
  19. 9 3
      src/views/unit-test/model/drag-question/index.module.less
  20. 2 2
      src/views/unit-test/model/drag-question/index.tsx
  21. 32 12
      src/views/unit-test/model/keep-look-question/index.module.less
  22. 239 61
      src/views/unit-test/model/keep-look-question/index.tsx
  23. 87 0
      src/views/unit-test/model/play-question/index.module.less
  24. 82 0
      src/views/unit-test/model/play-question/index.tsx
  25. 46 0
      src/views/unit-test/practice-mode/index.module.less
  26. 189 0
      src/views/unit-test/practice-mode/index.tsx
  27. 46 0
      src/views/unit-test/test-exercise/index.module.less
  28. 189 0
      src/views/unit-test/test-exercise/index.tsx
  29. 23 13
      src/views/unit-test/unit-detail/index.tsx

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

@@ -102,6 +102,22 @@ export const router: RouteRecordRaw[] = [
     meta: {
       title: '测验详情'
     }
+  },
+  {
+    path: '/test-exercise',
+    name: 'test-exercise',
+    component: () => import('@/views/unit-test/test-exercise'),
+    meta: {
+      title: '测试练习'
+    }
+  },
+  {
+    path: '/practice-mode',
+    name: 'practice-mode',
+    component: () => import('@/views/unit-test/practice-mode'),
+    meta: {
+      title: '测试模式'
+    }
   }
 ]
 

+ 1 - 3
src/school/approval-manage/batch-adjust.tsx

@@ -61,12 +61,10 @@ export default defineComponent({
             endTime: forms.endTime.join('-')
           }
         })
-        console.log(data)
         router.push({
           path: '/course-preview',
           query: {
-            cacheId: data,
-            type: 'change'
+            cacheId: data
           }
         })
       } catch {

+ 12 - 0
src/school/approval-manage/course-adjust.tsx

@@ -194,6 +194,18 @@ export default defineComponent({
                 placeholder="请选择课程开始时间"
                 modelValue={forms.startTime ? dayjs(forms.startTime).format('HH:mm') : ''}
                 onClick={() => {
+                  let freeTimeCount = 0
+                  const timeDetailList = state.timerList.timeDetailList || []
+                  timeDetailList.forEach((item: any) => {
+                    if (item.enable === true) {
+                      freeTimeCount += 1
+                    }
+                  })
+                  // 判断是否有排课时间段
+                  if (freeTimeCount <= 0) {
+                    showToast('当前没有可排课时间段,请重新选择课程开始日期')
+                    return
+                  }
                   state.showPopoverCourseTime = true
                 }}
               />

+ 1 - 1
src/school/companion-teacher/unbind.tsx

@@ -94,7 +94,7 @@ export default defineComponent({
               path: '/course-preview',
               query: {
                 cacheId: data.cacheId,
-                type: 'change'
+                type: 'unbind'
               }
             })
           }

+ 5 - 4
src/school/train-planning/component/course-preview/index.tsx

@@ -51,7 +51,7 @@ export default defineComponent({
             ? { orchestraId: forms.selectOrchestraId }
             : forms.planList.orchestra[0]
           state.tabValue = selectOrchestra.orchestraId
-          console.log(forms.selectClassGroupId, forms.planList.classes[selectOrchestra.orchestraId])
+          // console.log(forms.selectClassGroupId, forms.planList.classes[selectOrchestra.orchestraId])
           const selectClasses = forms.selectClassGroupId
             ? { classGroupId: forms.selectClassGroupId }
             : forms.planList.classes[selectOrchestra.orchestraId]
@@ -62,13 +62,13 @@ export default defineComponent({
 
           state.courseValue = selectClasses.classGroupId
 
-          console.log(selectClasses.classGroupId, 'selectClasses.classGroupId')
+          // console.log(selectClasses.classGroupId, 'selectClasses.classGroupId')
 
           // 判断是否有数据
           forms.selectOrchestraId = null
           forms.selectClassGroupId = null
 
-          console.log(selectClasses.classGroupId, 'selectClasses.classGroupId 333333')
+          // console.log(selectClasses.classGroupId, 'selectClasses.classGroupId 333333')
         }
       } catch {
         //
@@ -163,7 +163,8 @@ export default defineComponent({
         }, 100)
         setTimeout(() => {
           state.isClick = false
-          if (route.query.type === 'change') {
+          // 伴学指导交接
+          if (route.query.type === 'unbind') {
             router.replace('/companion-teacher')
           } else {
             postMessage({ api: 'back', content: {} })

+ 16 - 13
src/school/train-planning/modal/timer/index.tsx

@@ -24,7 +24,7 @@ export default defineComponent({
   setup(props, { slots, attrs, emit }) {
     const defaultTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
     const state = reactive({
-      calendarDate: null,
+      calendarDate: null as any,
       selectTimeStatus: false,
       selectTime: null as any,
       useTimer: [] as any, // 可排课时间段
@@ -45,18 +45,20 @@ export default defineComponent({
     }
 
     const onFilter = (type: any, option: any) => {
-      // console.log(type, option)
       // 时间
       if (type === 'hour') {
         const hour: any = []
         option.forEach((o: any) => {
           state.useTimerFormat.forEach((time: any) => {
-            if (o.value >= time.startHour && o.value <= time.endHour) {
+            if (
+              o.value >= time.startHour &&
+              o.value <= time.endHour &&
+              o.value != hour[hour.length - 1]?.value
+            ) {
               hour.push(o)
             }
           })
         })
-        // console.log(hour, 'hour')
         return hour
       }
       return option
@@ -109,13 +111,13 @@ export default defineComponent({
       // console.log(dayjs(tempDate).format('YYYY-MM-DD HH:mm:ss'), 'first', dayjs(tempDate).minute())
       // 时间加上每节课的时间,
       const lastDate = dayjs(tempDate).minute(props.times + dayjs(tempDate).minute())
-      console.log(dayjs(lastDate).format('YYYY-MM-DD HH:mm:ss'), 'second')
-      console.log(
-        dayjs(tempDate).format('YYYY-MM-DD HH:mm:ss'),
-        tempDate.toDate(),
-        'second tempDate',
-        state.useTimer
-      )
+      // console.log(dayjs(lastDate).format('YYYY-MM-DD HH:mm:ss'), 'second')
+      // console.log(
+      //   dayjs(tempDate).format('YYYY-MM-DD HH:mm:ss'),
+      //   tempDate.toDate(),
+      //   'second tempDate',
+      //   state.useTimer
+      // )
 
       let isActive = false
       state.useTimer.forEach((item: any) => {
@@ -152,6 +154,7 @@ export default defineComponent({
       console.log(props.timerList, 'timerList')
       state.calendarDate = props.timerList?.calendarDate
       const timeDetailList = props.timerList?.timeDetailList || []
+
       const useTimer: any = [] // 可排课时间段
       const usedTimer: any = [] // 不可排课时间段
 
@@ -167,8 +170,8 @@ export default defineComponent({
       const useFormat = onFormatTimer(useTimer)
       state.useTimerFormat = useFormat
       state.usedTimer = [...usedTimer]
-      // console.log(onFormatTimer(useTimer), 'onFormatTimer')
-      // console.log(state.useTimer, state.usedTimer, 'onUseTimer')
+      console.log(onFormatTimer(useTimer), 'onFormatTimer')
+      console.log(state.useTimer, state.usedTimer, 'onUseTimer')
 
       // 判断有可排课数据
       if (useFormat.length > 0) {

TEMPAT SAMPAH
src/student/download/images/manage_bg.png


TEMPAT SAMPAH
src/student/download/images/student_bg.png


TEMPAT SAMPAH
src/student/download/images/teacher_bg.png


+ 22 - 3
src/student/download/index.module.less

@@ -4,10 +4,29 @@
   background-size: cover;
 }
 
+.teacher {
+  background: url('./images/teacher_bg.png') no-repeat top center;
+  min-height: 100vh;
+  background-size: cover;
+}
+
+.manage {
+  background: url('./images/manage_bg.png') no-repeat top center;
+  min-height: 100vh;
+  background-size: cover;
+}
+
+.buttonGroup {
+  text-align: center;
+}
 .btn {
-  // style={{ width: '60%', margin: '0 20%' }}
-  width: 60%;
-  margin: 85vh 20% 0;
+  // position: absolute;
+  // bottom: 53px;
+  // left: 50%;
+  // margin-left: -98px;
+  width: 194px;
+  height: 44px;
+  line-height: 44px;
 }
 
 .wxpopup {

+ 23 - 5
src/student/download/index.tsx

@@ -1,9 +1,13 @@
-import { Button, showToast } from 'vant'
+import { Button, showToast, Image } from 'vant'
 import { defineComponent, onMounted, reactive } from 'vue'
 import { useRoute } from 'vue-router'
 import styles from './index.module.less'
 import wxBg from './images/wx_bg.png'
 import { browser } from '@/helpers/utils'
+import student from './images/student_bg.png'
+import teacher from './images/teacher_bg.png'
+import manage from './images/manage_bg.png'
+import OSticky from '@/components/o-sticky'
 
 // 唤起前缀
 // BandMusicTeam:// 管乐团
@@ -75,10 +79,24 @@ export default defineComponent({
       document.title = state.buttonText
     })
     return () => (
-      <div class={[styles.student]}>
-        <Button round size="large" color="#FF8057" class={styles.btn} onClick={onDownload}>
-          {state.buttonText}
-        </Button>
+      <div
+        class={[
+          styles.student,
+          state.type === 'teacher' && styles.teacher,
+          state.type === 'manage' && styles.manage
+        ]}
+      >
+        {/* {state.type !== 'teacher' && state.type !== 'manage' && <Image src={student} />}
+        {state.type === 'teacher' && <Image src={teacher} />}
+        {state.type === 'manage' && <Image src={manage} />} */}
+
+        <OSticky background="white" position="bottom">
+          <div class={[styles.buttonGroup, 'btnGroup']}>
+            <Button round size="large" color="#FF8057" class={styles.btn} onClick={onDownload}>
+              {state.buttonText}
+            </Button>
+          </div>
+        </OSticky>
 
         {state.wxStatus && (
           <div

+ 8 - 4
src/student/download/transfer.tsx

@@ -4,6 +4,7 @@ import { defineComponent } from 'vue'
 import styles from './index.module.less'
 import wxBg from './images/wx_bg.png'
 import qs from 'query-string'
+import OSticky from '@/components/o-sticky'
 
 export default defineComponent({
   name: 'download-transfer',
@@ -92,10 +93,13 @@ export default defineComponent({
   render() {
     return (
       <div class={[styles.student]}>
-        <Button round size="large" color="#FF8057" class={styles.btn} onClick={this.onDownload}>
-          {this.buttonText}
-        </Button>
-
+        <OSticky background="white" position="bottom">
+          <div class={[styles.buttonGroup, 'btnGroup']}>
+            <Button round size="large" color="#FF8057" class={styles.btn} onClick={this.onDownload}>
+              {this.buttonText}
+            </Button>
+          </div>
+        </OSticky>
         {this.wxStatus && (
           <div
             class={styles.wxpopup}

+ 3 - 8
src/student/music-group/pre-apply/component/apply.tsx

@@ -374,20 +374,15 @@ export default defineComponent({
 
         <Dialog
           v-model:show={state.subjectChangeStatus}
-          message={'您修改了乐团声部,若确认更换,请先将原声部订单退款,完成后修改声部重新缴费。'}
+          message={'您已报名,不能更换声部'}
           messageAlign="left"
-          confirmButtonText="去退款"
-          cancelButtonText="选错了"
-          showCancelButton
-          onConfirm={() => {
-            emit('next', 'order')
-          }}
+          confirmButtonText="确定"
         >
           {{
             title: () => (
               <div class={styles.dialogTitle}>
                 <i></i>
-                修改声部
+                提示
               </div>
             )
           }}

+ 1 - 1
src/student/music-group/shop-address/address-operation.tsx

@@ -132,7 +132,7 @@ export default defineComponent({
           />
           <Field
             label="所在地区"
-            placeholder="省、市、区、街道"
+            placeholder="省/市/区/街道"
             readonly
             isLink
             modelValue={state.pcrStr}

TEMPAT SAMPAH
src/views/unit-test/images/icon-song.png


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

@@ -119,7 +119,15 @@ export default defineComponent({
                         <div class={styles.endTime}>截止时间:2022-10-24 21:00</div>
 
                         <div class={styles.unitBtnGroup}>
-                          <Button color="#FFF0E6" round block style={{ color: '#F67146' }}>
+                          <Button
+                            color="#FFF0E6"
+                            round
+                            block
+                            style={{ color: '#F67146' }}
+                            onClick={() => {
+                              router.push('/test-exercise')
+                            }}
+                          >
                             练习模式
                           </Button>
                           <Button

+ 9 - 3
src/views/unit-test/model/choice-question/index.module.less

@@ -6,9 +6,9 @@
   border-radius: 10px;
 }
 .unitSubjectTitle {
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
+  // display: flex;
+  // align-items: center;
+  // flex-wrap: wrap;
   font-size: 16px;
   font-weight: 500;
   color: #333333;
@@ -16,6 +16,12 @@
   .unitScore {
     color: #777777;
   }
+  :global {
+    .van-tag {
+      vertical-align: middle;
+      margin-top: -3px;
+    }
+  }
 }
 .unitTitleImg {
   padding-top: 20px;

+ 5 - 0
src/views/unit-test/model/choice-question/index.tsx

@@ -18,6 +18,10 @@ export default defineComponent({
     answers: {
       type: Object,
       default: {}
+    },
+    readOnly: {
+      type: Boolean,
+      default: false
     }
   },
   emits: ['update:value'],
@@ -44,6 +48,7 @@ export default defineComponent({
     })
 
     const onSelect = (item: any) => {
+      if (props.readOnly) return
       if (props.type === 'checkbox') {
         // 判断是否已选过
         const value: any = props.value

+ 9 - 3
src/views/unit-test/model/drag-question/index.module.less

@@ -7,9 +7,9 @@
 }
 
 .unitSubjectTitle {
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
+  // display: flex;
+  // align-items: center;
+  // flex-wrap: wrap;
   font-size: 16px;
   font-weight: 500;
   color: #333333;
@@ -17,6 +17,12 @@
   .unitScore {
     color: #777777;
   }
+  :global {
+    .van-tag {
+      vertical-align: middle;
+      margin-top: -3px;
+    }
+  }
 }
 .unitTitleSection {
   margin-top: 20px;

+ 2 - 2
src/views/unit-test/model/drag-question/index.tsx

@@ -25,7 +25,7 @@ export default defineComponent({
     /* 只读 */
     readOnly: {
       type: Boolean,
-      default: true
+      default: false
     }
   },
   emits: ['update:value'],
@@ -79,7 +79,7 @@ export default defineComponent({
       nextTick(() => {
         const el = document.getElementById(state.domId)
         state.sortable = Sortable.create(el, {
-          // disabled: true,
+          disabled: props.readOnly,
           animation: 150,
           sort: true,
           fallbackTolerance: 3,

+ 32 - 12
src/views/unit-test/model/keep-look-question/index.module.less

@@ -7,9 +7,9 @@
 }
 
 .unitSubjectTitle {
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
+  // display: flex;
+  // align-items: center;
+  // flex-wrap: wrap;
   font-size: 16px;
   font-weight: 500;
   color: #333333;
@@ -17,6 +17,12 @@
   .unitScore {
     color: #777777;
   }
+  :global {
+    .van-tag {
+      vertical-align: middle;
+      margin-top: -3px;
+    }
+  }
 }
 
 .unitTitleImg {
@@ -28,15 +34,17 @@
 .unitAnswers {
   position: relative;
   padding-bottom: 20px;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
 
-  .leftSection,
-  .rightSection {
+  .answerItem {
     position: relative;
-    width: 95px;
     z-index: 2;
+    margin-bottom: 15px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    &:last-child {
+      margin-bottom: 0;
+    }
   }
   .img {
     width: 95px;
@@ -50,8 +58,7 @@
     border: 2px solid #d5d5d5;
     overflow: hidden;
     height: 54px;
-    width: 100%;
-    margin-bottom: 15px;
+    width: 95px;
     font-size: 16px;
     font-weight: 500;
     color: #333333;
@@ -66,7 +73,20 @@
 
 .canvasSection {
   position: absolute;
-  background: #f9f9f9;
   top: 0;
   left: 0;
 }
+
+.resetBtnGroup {
+  text-align: right;
+  padding-bottom: 16px;
+  :global {
+    .van-button {
+      min-width: 84px;
+      line-height: 31px;
+      height: 31px;
+      font-size: 15px;
+      font-weight: 500;
+    }
+  }
+}

+ 239 - 61
src/views/unit-test/model/keep-look-question/index.tsx

@@ -1,9 +1,6 @@
 import { Tag, Image, Button } from 'vant'
-import { defineComponent, nextTick, onMounted, PropType, reactive } from 'vue'
-import { labelOptions } from '../../unit'
+import { defineComponent, nextTick, onMounted, PropType, reactive, ref } from 'vue'
 import styles from './index.module.less'
-import Sortable from 'sortablejs'
-import deepClone from '@/helpers/deep-clone'
 import { useRect } from '@vant/use'
 
 // 单选和多选题
@@ -25,18 +22,16 @@ export default defineComponent({
     /* 只读 */
     readOnly: {
       type: Boolean,
-      default: true
+      default: false
     }
   },
   emits: ['update:value'],
   setup(props, { emit }) {
+    const canvasRef = ref()
     const state = reactive({
       answerDomId: 'answer' + +new Date(),
-      answerObj: {
-        height: 100,
-        width: 100
-      },
-      canvasDomId: 'canvas' + +new Date(),
+      answerRect: {} as any,
+
       sortable: null as any,
       list: [] as any,
       options: [
@@ -45,70 +40,198 @@ export default defineComponent({
           value: 'Sol',
           left: false,
           right: false,
-          locked: false // 是否已经连线
+          leftLocked: false, // 是否已经连线
+          rightLocked: false // 是否已经连线
         },
         {
           index: 2,
           value: 'Sal',
           left: false,
           right: false,
-          locked: false
+          leftLocked: false, // 是否已经连线
+          rightLocked: false // 是否已经连线
         },
         {
           index: 3,
           value: 'La',
           left: false,
           right: false,
-          locked: false
+          leftLocked: false, // 是否已经连线
+          rightLocked: false // 是否已经连线
         },
         {
           index: 4,
           value: 'Si',
           left: false,
           right: false,
-          locked: false
+          leftLocked: false, // 是否已经连线
+          rightLocked: false // 是否已经连线
         }
       ],
+      drawLineList: [] as any,
       selectItem: [] as any
     })
-
+    /* 
+    drawLineList 下的对象 
+      {
+          startPoint: { x: 0, y: 0 },
+          endPoint: { x: 0, y: 0},
+          leftIndex: 1,
+          rightIndex: 2
+        }
+    */
     const onLeftClick = (e: any, item: any) => {
-      const obj = useRect(e.target)
-      console.log(e, obj)
-      state.options.forEach((item: any) => {
-        item.left = false
+      // 是否只读
+      if (props.readOnly) return
+      state.options.forEach((option: any) => {
+        if (!option.leftLocked && item.index !== option.index) {
+          option.left = false
+        }
       })
-      item.left = !item.left
 
-      // 为true时添加定位
-      if (item.left) {
-        state.selectItem[0] = obj
+      const selectd = isDrawLine(item.index, 'left')
+      // 判断当前元素是否已经连线了
+      if (selectd.status) {
+        state.options.forEach((option: any) => {
+          if (!option.rightLocked) {
+            option.right = false
+          }
+        })
+        state.selectItem = []
+        // 如果已经连线了则去掉
+        state.drawLineList.splice(selectd.selectIndex, 1)
+        // 重新绘制
+        renderDrawLine(canvasRef.value)
+
+        state.options.forEach((option: any) => {
+          if (selectd.selectOption.leftIndex === option.index) {
+            option.left = false
+            option.leftLocked = false
+          }
+          if (selectd.selectOption.rightIndex === option.index) {
+            option.right = false
+            option.rightLocked = false
+          }
+        })
+      } else {
+        item.left = !item.left
+        if (item.left) {
+          // 为true时添加定位
+          const obj: any = useRect(e.target)
+          obj.index = item.index
+          state.selectItem[0] = obj
+        } else {
+          state.selectItem[0] = null
+        }
+
+        // 判断是否有二个的关联
+        if (state.selectItem[0] && state.selectItem[1]) {
+          const postion = calcPoint()
+          state.drawLineList.push(postion)
+          state.selectItem = []
+
+          renderDrawLine(canvasRef.value)
+        }
       }
     }
 
     const onRightClick = (e: any, item: any) => {
-      const obj = useRect(e.target)
-      console.log(e, obj)
-      state.options.forEach((item: any) => {
-        item.right = false
+      // 是否只读
+      if (props.readOnly) return
+      // 去掉
+      state.options.forEach((option: any) => {
+        if (!option.rightLocked && item.index !== option.index) {
+          option.right = false
+        }
       })
-      item.right = !item.right
+      const selectd = isDrawLine(item.index, 'right')
+
+      // 判断当前元素是否已经连线了
+      if (selectd.status) {
+        state.options.forEach((option: any) => {
+          if (!option.leftLocked) {
+            option.left = false
+          }
+        })
+        state.selectItem = []
+        // console.log(
+        //   selectd,
+        //   'selected',
+        //   JSON.stringify(state.drawLineList),
+        //   state.drawLineList,
+        //   state.drawLineList.length
+        // )
+        // 如果已经连线了则去掉
+        state.drawLineList.splice(selectd.selectIndex, 1)
+        // 重新绘制
+        renderDrawLine(canvasRef.value)
 
-      // 为true时添加定位
-      if (item.right) {
-        state.selectItem[1] = obj
+        state.options.forEach((option: any) => {
+          if (selectd.selectOption.leftIndex === option.index) {
+            option.left = false
+            option.leftLocked = false
+          }
+          if (selectd.selectOption.rightIndex === option.index) {
+            option.right = false
+            option.rightLocked = false
+          }
+        })
+      } else {
+        item.right = !item.right
+
+        if (item.right) {
+          // 为true时添加定位
+          const obj: any = useRect(e.target)
+          obj.index = item.index
+          state.selectItem[1] = obj
+        } else {
+          state.selectItem[1] = null
+        }
 
-        if (state.selectItem.length >= 2) {
-          drawLine()
+        // 判断是否有二个的关联
+        if (state.selectItem[0] && state.selectItem[1]) {
+          const postion = calcPoint()
+          state.drawLineList.push(postion)
+          state.selectItem = []
+          renderDrawLine(canvasRef.value)
         }
       }
     }
 
-    const drawLine = () => {
-      const canvas: any = document.getElementById(state.canvasDomId)
-      const canvasPostion = useRect(canvas)
-      console.log(canvasPostion, 'canvasPostion')
+    /**
+     * @description 判断是否已连接
+     * @param key 选中的选项标识
+     * @returns status 状态,  selectIndex 索引, selectOption 选中选项
+     */
+    const isDrawLine = (key: number, type = 'left') => {
+      const drawLineList = state.drawLineList || []
+      let status = false // true 连,false 没连
+      let selectIndex = 0 // 连了所在的索引
+      let selectOption: any = {}
+      drawLineList.forEach((item: any, index: number) => {
+        if (item.leftIndex === key && type === 'left') {
+          selectOption = item
+          status = true
+          selectIndex = index
+        } else if (item.rightIndex === key && type === 'right') {
+          selectOption = item
+          status = true
+          selectIndex = index
+        }
+      })
+      return {
+        status,
+        selectIndex,
+        selectOption
+      }
+    }
 
+    /**
+     * @description 计算连线坐标位置及左右关联编号
+     * @returns 连线的坐标
+     */
+    const calcPoint = () => {
+      const canvasPostion = useRect(canvasRef.value)
       const firstPostion = state.selectItem[0]
       const secondPostion = state.selectItem[1]
 
@@ -122,9 +245,45 @@ export default defineComponent({
         y: secondPostion.top + secondPostion.height / 2 - canvasPostion.top
       }
 
-      const ctx = canvas.getContext('2d')
-      console.log(startPoint, endPoint, ctx)
+      state.options.forEach((item: any) => {
+        if (item.index === firstPostion.index) {
+          item.leftLocked = true
+        }
+        if (item.index === secondPostion.index) {
+          item.rightLocked = true
+        }
+      })
+      return {
+        startPoint,
+        endPoint,
+        leftIndex: firstPostion.index,
+        rightIndex: secondPostion.index
+      }
+    }
+
+    /**
+     * @description 绘制连线
+     * @param canvasRef 对象
+     */
+    const renderDrawLine = (canvasRef: any) => {
+      // 重新画线
+      if (canvasRef.getContext) {
+        const ctx = canvasRef.getContext('2d')
+        ctx.clearRect(0, 0, state.answerRect.width, state.answerRect.height)
+        state.drawLineList.forEach((item: any) => {
+          drawLine(ctx, item.startPoint, item.endPoint)
+        })
+      }
+    }
 
+    /**
+     * @description 连线
+     * @param ctx canvas 对象
+     * @param startPoint 开始坐标
+     * @param endPoint 结束坐标
+     * @returns void(0)
+     */
+    const drawLine = (ctx: any, startPoint: any, endPoint: any) => {
       ctx.beginPath()
       ctx.moveTo(startPoint.x, startPoint.y)
       ctx.lineTo(endPoint.x, endPoint.y)
@@ -150,13 +309,13 @@ export default defineComponent({
     // }
 
     onMounted(() => {
-      const answer: any = document.getElementById(state.answerDomId)
-      const { width, height } = useRect(answer)
-      console.log(width, height)
-      state.answerObj = {
-        height,
-        width
-      }
+      // 获取canvas 对象
+      nextTick(() => {
+        // 获取canvas元素定位
+        const answer: any = document.getElementById(state.answerDomId)
+        const answerRect = useRect(answer)
+        state.answerRect = answerRect
+      })
     })
 
     return () => (
@@ -172,8 +331,14 @@ export default defineComponent({
         />
 
         <div class={[styles.unitAnswers]} id={state.answerDomId}>
-          <div class={styles.leftSection}>
-            {state.options.map((item: any) => (
+          <canvas
+            ref={canvasRef}
+            class={styles.canvasSection}
+            width={state.answerRect.width || 0}
+            height={state.answerRect.height || 0}
+          ></canvas>
+          {state.options.map((item: any) => (
+            <div class={styles.answerItem}>
               <div
                 class={[styles.unitItem, item.left && styles.active]}
                 onClick={(e: any) => onLeftClick(e, item)}
@@ -183,25 +348,38 @@ export default defineComponent({
                   class={styles.img}
                 />
               </div>
-            ))}
-          </div>
-          <div class={styles.rightSection}>
-            {state.options.map((item: any) => (
               <div
                 class={[styles.unitItem, item.right && styles.active]}
                 onClick={(e: any) => onRightClick(e, item)}
               >
-                Re{item.index}
+                {item.value}
+                {item.index}
               </div>
-            ))}
-          </div>
+            </div>
+          ))}
+        </div>
+        <div class={styles.resetBtnGroup}>
+          <Button
+            round
+            type="primary"
+            disabled={props.readOnly}
+            onClick={() => {
+              state.drawLineList = []
+              renderDrawLine(canvasRef.value)
 
-          <canvas
-            id={state.canvasDomId}
-            class={styles.canvasSection}
-            width={state.answerObj.width}
-            height={state.answerObj.height}
-          ></canvas>
+              // 清除所有的选中状态
+              state.options.forEach((item: any) => {
+                item.left = false
+                item.right = false
+                item.leftLocked = false
+                item.rightLocked = false
+              })
+              // 清除所有的选择的内容
+              state.selectItem = []
+            }}
+          >
+            重置
+          </Button>
         </div>
       </div>
     )

+ 87 - 0
src/views/unit-test/model/play-question/index.module.less

@@ -0,0 +1,87 @@
+.unitSubject {
+  padding: 15px;
+  margin: 0 13px;
+  background-color: #fff;
+  // overflow: hidden;
+  border-radius: 10px;
+}
+
+.unitSubjectTitle {
+  // display: flex;
+  // align-items: center;
+  // flex-wrap: wrap;
+  font-size: 16px;
+  font-weight: 500;
+  color: #333333;
+  line-height: 26px;
+  .unitScore {
+    color: #777777;
+  }
+  :global {
+    .van-tag {
+      vertical-align: middle;
+      margin-top: -3px;
+    }
+  }
+}
+.unitTitleSection {
+  margin-top: 20px;
+  border-radius: 6px;
+  border: 1px solid #d5d5d5;
+  padding: 25px 5px;
+}
+.unitTitleImg {
+  width: 100%;
+}
+
+.unitAnswers {
+  padding-top: 20px;
+  padding-bottom: 30px;
+}
+
+.playSection {
+  padding: 15px 12px;
+  background: #ffebdd;
+  border-radius: 10px;
+  .img {
+    width: 45px;
+    height: 45px;
+    margin-right: 12px;
+  }
+
+  .playTitle {
+    font-size: 16px;
+    font-weight: 500;
+    color: #333333;
+  }
+  .playBtn {
+    flex-shrink: 0;
+    font-size: 15px;
+    font-weight: 500;
+    color: #ffffff;
+    height: 31px;
+    line-height: 31px;
+  }
+}
+
+.unitScoreNum {
+  margin-top: 25px;
+  padding-top: 20px;
+  text-align: center;
+  .score {
+    font-size: 32px;
+    font-weight: bold;
+    color: #f67146;
+  }
+  .scoreTitle {
+    font-size: 16px;
+    font-weight: 500;
+    color: #333333;
+    padding: 4px 0 10px;
+  }
+
+  .scoreTips {
+    font-size: 12px;
+    color: #aaaaaa;
+  }
+}

+ 82 - 0
src/views/unit-test/model/play-question/index.tsx

@@ -0,0 +1,82 @@
+import { Tag, Image, Button, Cell, Icon } from 'vant'
+import { defineComponent, nextTick, onMounted, PropType, reactive } from 'vue'
+import styles from './index.module.less'
+import deepClone from '@/helpers/deep-clone'
+import iconSong from '../../images/icon-song.png'
+
+// 单选和多选题
+export default defineComponent({
+  name: 'choice-question',
+  props: {
+    value: {
+      type: [String, Number, Array],
+      default: ''
+    },
+    answers: {
+      type: Object,
+      default: {}
+    },
+    /* 只读 */
+    readOnly: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['update:value'],
+  setup(props, { emit }) {
+    const state = reactive({
+      list: [] as any
+    })
+
+    // const onSelect = (item: any) => {
+    //   if (props.type === 'checkbox') {
+    //     // 判断是否已选过
+    //     const value: any = props.value
+    //     if (value.includes(item.index)) {
+    //       const index = value.findIndex((v: any) => v === item.index)
+    //       value.splice(index, 1)
+    //       emit('update:value', [...value])
+    //     } else {
+    //       emit('update:value', [item.index, ...value])
+    //     }
+    //   } else {
+    //     emit('update:value', item.index)
+    //   }
+    // }
+    return () => (
+      <div class={styles.unitSubject}>
+        <div class={styles.unitSubjectTitle}>
+          4、请点击以下曲目进行评测,评测分数达到80分合格
+          <span class={styles.unitScore}>(5分)</span>
+          <Tag type="primary">演奏题</Tag>
+        </div>
+        <div class={styles.unitTitleSection}>
+          <Image
+            class={styles.unitTitleImg}
+            src="https://lanhu-dds-backend.oss-cn-beijing.aliyuncs.com/merge_image/imgs/dbb27307d428424c8efb9f26032cfa1a_mergeImage.png"
+          />
+        </div>
+
+        <div class={[styles.unitAnswers]}>
+          <Cell class={styles.playSection} center titleClass={['van-ellipsis', styles.playTitle]}>
+            {{
+              icon: () => <Image class={styles.img} src={iconSong} />,
+              title: () => <>没开机三江源没开机三江源</>,
+              value: () => (
+                <Button round class={styles.playBtn} type="primary">
+                  点击评测
+                  <Icon name="play" />
+                </Button>
+              )
+            }}
+          </Cell>
+          <div class={['van-hairline--top', styles.unitScoreNum]}>
+            <div class={styles.score}>89</div>
+            <div class={styles.scoreTitle}>评测分数</div>
+            <div class={styles.scoreTips}>多次评测取完整评测的最高分数</div>
+          </div>
+        </div>
+      </div>
+    )
+  }
+})

+ 46 - 0
src/views/unit-test/practice-mode/index.module.less

@@ -0,0 +1,46 @@
+.unitDetail {
+  overflow: hidden;
+}
+
+.unitSection {
+  margin: 12px 13px;
+  padding: 13px 15px;
+  width: auto;
+  overflow: hidden;
+  border-radius: 10px;
+
+  .unitTitle {
+    font-size: 16px;
+    font-weight: 500;
+    color: #333333;
+  }
+
+  .unitCount {
+    padding-top: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .qNums {
+    display: flex;
+    align-items: center;
+    font-size: 14px;
+    color: #333333;
+    line-height: 20px;
+    .num {
+      color: #f67146;
+    }
+  }
+  .icon {
+    width: 14px;
+    height: 14px;
+    margin-right: 4px;
+  }
+}
+
+.wapList {
+  width: 44px;
+  height: 44px;
+  flex-shrink: 0;
+  margin-left: 18px;
+}

+ 189 - 0
src/views/unit-test/practice-mode/index.tsx

@@ -0,0 +1,189 @@
+import {
+  ActionSheet,
+  Button,
+  Cell,
+  CountDown,
+  Icon,
+  Image,
+  Popup,
+  Swipe,
+  SwipeItem,
+  Tag
+} from 'vant'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import NoticeStart from '../model/notice-start'
+import styles from './index.module.less'
+import iconQuestionNums from '../images/icon-question-nums.png'
+import iconCountDown from '../images/icon-count-down.png'
+import iconButtonList from '../images/icon-button-list.png'
+import OSticky from '@/components/o-sticky'
+import ChoiceQuestion from '../model/choice-question'
+import AnswerList from '../model/answer-list'
+import ODialog from '@/components/o-dialog'
+import DragQuestion from '../model/drag-question'
+import KeepLookQuestion from '../model/keep-look-question'
+import PlayQuestion from '../model/play-question'
+
+export default defineComponent({
+  name: 'unit-detail',
+  setup() {
+    const route = useRoute()
+    const router = useRouter()
+    const countDownRef = ref()
+    const swipeRef = ref()
+    const state = reactive({
+      visiableNotice: false,
+      visiableAnswer: false,
+      currentIndex: 0,
+      questionList: [5],
+      answerList: {},
+      time: 30 * 60 * 1000,
+      visiableSure: false,
+      childs: [
+        { name: 'John', id: 0 },
+        { name: 'Joao', id: 1 },
+        { name: 'Jean', id: 2 }
+      ]
+    })
+
+    return () => (
+      <div class={styles.unitDetail}>
+        <Cell center class={styles.unitSection}>
+          {{
+            title: () => <div class={styles.unitTitle}>长笛level1上册测验一</div>,
+            label: () => (
+              <div class={styles.unitCount}>
+                <div class={styles.qNums}>
+                  <Icon class={styles.icon} name={iconQuestionNums} />
+                  题目数量 <span class={styles.num}>1</span>/4
+                </div>
+                <div class={styles.qNums}>
+                  <Icon class={styles.icon} name={iconCountDown} />
+                  剩余时长:
+                  <CountDown
+                    ref={countDownRef}
+                    time={state.time}
+                    format={'mm:ss'}
+                    autoStart={false}
+                  />
+                </div>
+              </div>
+            )
+          }}
+        </Cell>
+
+        <Swipe
+          loop={false}
+          showIndicators={false}
+          ref={swipeRef}
+          duration={300}
+          touchable={false}
+          lazyRender
+          // initialSwipe={state.currentIndex}
+          onChange={(index: number) => {
+            state.currentIndex = index
+          }}
+        >
+          <SwipeItem>
+            <ChoiceQuestion v-model:value={state.answerList[0]} type="checkbox" />
+          </SwipeItem>
+          <SwipeItem>
+            <ChoiceQuestion v-model:value={state.answerList[1]} type="radio" />
+          </SwipeItem>
+          <SwipeItem>
+            <DragQuestion />
+          </SwipeItem>
+          <SwipeItem>
+            <KeepLookQuestion />
+          </SwipeItem>
+          <SwipeItem>
+            <PlayQuestion />
+          </SwipeItem>
+        </Swipe>
+
+        <OSticky position="bottom" background="white">
+          <div class={['btnGroup btnMore']}>
+            {state.currentIndex > 0 && (
+              <Button
+                round
+                block
+                type="primary"
+                plain
+                onClick={() => {
+                  swipeRef.value?.prev()
+                }}
+              >
+                上一题
+              </Button>
+            )}
+            <Button
+              block
+              round
+              type="primary"
+              onClick={() => {
+                // if (state.questionList.length - 1 === state.currentIndex) {
+                //   state.visiableSure = true
+                // } else {
+                swipeRef.value?.next()
+                // }
+              }}
+            >
+              下一题
+              {/* {state.questionList.length === state.currentIndex + 1 ? '测试完成' : '下一题'} */}
+            </Button>
+            <Image
+              src={iconButtonList}
+              class={[styles.wapList, 'van-haptics-feedback']}
+              onClick={() => (state.visiableAnswer = true)}
+            />
+          </div>
+        </OSticky>
+
+        {/* 题目集合 */}
+        <ActionSheet v-model:show={state.visiableAnswer} title="题目列表" safeAreaInsetBottom>
+          <AnswerList
+            value={[1, 3, 4]}
+            onSelect={(item: any) => {
+              // 跳转,并且跳过动画
+              swipeRef.value?.swipeTo(item, {
+                immediate: true
+              })
+              state.visiableAnswer = false
+            }}
+          />
+        </ActionSheet>
+
+        {/* 测验须知 */}
+        <Popup
+          v-model:show={state.visiableNotice}
+          round
+          style={{ width: '90%' }}
+          closeOnClickOverlay={false}
+        >
+          <NoticeStart
+            onClose={() => {
+              state.visiableNotice = false
+              router.back()
+            }}
+            onConfirm={() => {
+              console.log('start')
+              countDownRef.value.start()
+              state.visiableNotice = false
+            }}
+          />
+        </Popup>
+
+        <ODialog
+          v-model:show={state.visiableSure}
+          title="测验完成"
+          message="确认本次测验的题目都完成了吗?\n提交后不可修改哦"
+          messageAlign="left"
+          showCancelButton
+          cancelButtonText="再等等"
+          confirmButtonText="确认完成"
+        />
+      </div>
+    )
+  }
+})

+ 46 - 0
src/views/unit-test/test-exercise/index.module.less

@@ -0,0 +1,46 @@
+.unitDetail {
+  overflow: hidden;
+}
+
+.unitSection {
+  margin: 12px 13px;
+  padding: 13px 15px;
+  width: auto;
+  overflow: hidden;
+  border-radius: 10px;
+
+  .unitTitle {
+    font-size: 16px;
+    font-weight: 500;
+    color: #333333;
+  }
+
+  .unitCount {
+    padding-top: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .qNums {
+    display: flex;
+    align-items: center;
+    font-size: 14px;
+    color: #333333;
+    line-height: 20px;
+    .num {
+      color: #f67146;
+    }
+  }
+  .icon {
+    width: 14px;
+    height: 14px;
+    margin-right: 4px;
+  }
+}
+
+.wapList {
+  width: 44px;
+  height: 44px;
+  flex-shrink: 0;
+  margin-left: 18px;
+}

+ 189 - 0
src/views/unit-test/test-exercise/index.tsx

@@ -0,0 +1,189 @@
+import {
+  ActionSheet,
+  Button,
+  Cell,
+  CountDown,
+  Icon,
+  Image,
+  Popup,
+  Swipe,
+  SwipeItem,
+  Tag
+} from 'vant'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import NoticeStart from '../model/notice-start'
+import styles from './index.module.less'
+import iconQuestionNums from '../images/icon-question-nums.png'
+import iconCountDown from '../images/icon-count-down.png'
+import iconButtonList from '../images/icon-button-list.png'
+import OSticky from '@/components/o-sticky'
+import ChoiceQuestion from '../model/choice-question'
+import AnswerList from '../model/answer-list'
+import ODialog from '@/components/o-dialog'
+import DragQuestion from '../model/drag-question'
+import KeepLookQuestion from '../model/keep-look-question'
+import PlayQuestion from '../model/play-question'
+
+export default defineComponent({
+  name: 'unit-detail',
+  setup() {
+    const route = useRoute()
+    const router = useRouter()
+    const countDownRef = ref()
+    const swipeRef = ref()
+    const state = reactive({
+      visiableNotice: false,
+      visiableAnswer: false,
+      currentIndex: 0,
+      questionList: [5],
+      answerList: {},
+      time: 30 * 60 * 1000,
+      visiableSure: false,
+      childs: [
+        { name: 'John', id: 0 },
+        { name: 'Joao', id: 1 },
+        { name: 'Jean', id: 2 }
+      ]
+    })
+
+    return () => (
+      <div class={styles.unitDetail}>
+        <Cell center class={styles.unitSection}>
+          {{
+            title: () => <div class={styles.unitTitle}>长笛level1上册测验一</div>,
+            label: () => (
+              <div class={styles.unitCount}>
+                <div class={styles.qNums}>
+                  <Icon class={styles.icon} name={iconQuestionNums} />
+                  题目数量 <span class={styles.num}>1</span>/4
+                </div>
+                <div class={styles.qNums}>
+                  <Icon class={styles.icon} name={iconCountDown} />
+                  剩余时长:
+                  <CountDown
+                    ref={countDownRef}
+                    time={state.time}
+                    format={'mm:ss'}
+                    autoStart={false}
+                  />
+                </div>
+              </div>
+            )
+          }}
+        </Cell>
+
+        <Swipe
+          loop={false}
+          showIndicators={false}
+          ref={swipeRef}
+          duration={300}
+          touchable={false}
+          lazyRender
+          // initialSwipe={state.currentIndex}
+          onChange={(index: number) => {
+            state.currentIndex = index
+          }}
+        >
+          <SwipeItem>
+            <ChoiceQuestion v-model:value={state.answerList[0]} type="checkbox" />
+          </SwipeItem>
+          <SwipeItem>
+            <ChoiceQuestion v-model:value={state.answerList[1]} type="radio" />
+          </SwipeItem>
+          <SwipeItem>
+            <DragQuestion />
+          </SwipeItem>
+          <SwipeItem>
+            <KeepLookQuestion />
+          </SwipeItem>
+          <SwipeItem>
+            <PlayQuestion />
+          </SwipeItem>
+        </Swipe>
+
+        <OSticky position="bottom" background="white">
+          <div class={['btnGroup btnMore']}>
+            {state.currentIndex > 0 && (
+              <Button
+                round
+                block
+                type="primary"
+                plain
+                onClick={() => {
+                  swipeRef.value?.prev()
+                }}
+              >
+                上一题
+              </Button>
+            )}
+            <Button
+              block
+              round
+              type="primary"
+              onClick={() => {
+                // if (state.questionList.length - 1 === state.currentIndex) {
+                //   state.visiableSure = true
+                // } else {
+                swipeRef.value?.next()
+                // }
+              }}
+            >
+              下一题
+              {/* {state.questionList.length === state.currentIndex + 1 ? '测试完成' : '下一题'} */}
+            </Button>
+            <Image
+              src={iconButtonList}
+              class={[styles.wapList, 'van-haptics-feedback']}
+              onClick={() => (state.visiableAnswer = true)}
+            />
+          </div>
+        </OSticky>
+
+        {/* 题目集合 */}
+        <ActionSheet v-model:show={state.visiableAnswer} title="题目列表" safeAreaInsetBottom>
+          <AnswerList
+            value={[1, 3, 4]}
+            onSelect={(item: any) => {
+              // 跳转,并且跳过动画
+              swipeRef.value?.swipeTo(item, {
+                immediate: true
+              })
+              state.visiableAnswer = false
+            }}
+          />
+        </ActionSheet>
+
+        {/* 测验须知 */}
+        <Popup
+          v-model:show={state.visiableNotice}
+          round
+          style={{ width: '90%' }}
+          closeOnClickOverlay={false}
+        >
+          <NoticeStart
+            onClose={() => {
+              state.visiableNotice = false
+              router.back()
+            }}
+            onConfirm={() => {
+              console.log('start')
+              countDownRef.value.start()
+              state.visiableNotice = false
+            }}
+          />
+        </Popup>
+
+        <ODialog
+          v-model:show={state.visiableSure}
+          title="测验完成"
+          message="确认本次测验的题目都完成了吗?\n提交后不可修改哦"
+          messageAlign="left"
+          showCancelButton
+          cancelButtonText="再等等"
+          confirmButtonText="确认完成"
+        />
+      </div>
+    )
+  }
+})

+ 23 - 13
src/views/unit-test/unit-detail/index.tsx

@@ -23,6 +23,7 @@ import AnswerList from '../model/answer-list'
 import ODialog from '@/components/o-dialog'
 import DragQuestion from '../model/drag-question'
 import KeepLookQuestion from '../model/keep-look-question'
+import PlayQuestion from '../model/play-question'
 
 export default defineComponent({
   name: 'unit-detail',
@@ -84,13 +85,21 @@ export default defineComponent({
             state.currentIndex = index
           }}
         >
-          {state.questionList.map((item: any) => (
-            <SwipeItem>
-              {/* <ChoiceQuestion v-model:value={state.answerList[item]} type="checkbox" /> */}
-              {/* <DragQuestion /> */}
-              <KeepLookQuestion />
-            </SwipeItem>
-          ))}
+          <SwipeItem>
+            <ChoiceQuestion v-model:value={state.answerList[0]} type="checkbox" />
+          </SwipeItem>
+          <SwipeItem>
+            <ChoiceQuestion v-model:value={state.answerList[1]} type="radio" />
+          </SwipeItem>
+          <SwipeItem>
+            <DragQuestion />
+          </SwipeItem>
+          <SwipeItem>
+            <KeepLookQuestion />
+          </SwipeItem>
+          <SwipeItem>
+            <PlayQuestion />
+          </SwipeItem>
         </Swipe>
 
         <OSticky position="bottom" background="white">
@@ -113,14 +122,15 @@ export default defineComponent({
               round
               type="primary"
               onClick={() => {
-                if (state.questionList.length - 1 === state.currentIndex) {
-                  state.visiableSure = true
-                } else {
-                  swipeRef.value?.next()
-                }
+                // if (state.questionList.length - 1 === state.currentIndex) {
+                //   state.visiableSure = true
+                // } else {
+                swipeRef.value?.next()
+                // }
               }}
             >
-              {state.questionList.length === state.currentIndex + 1 ? '测试完成' : '下一题'}
+              下一题
+              {/* {state.questionList.length === state.currentIndex + 1 ? '测试完成' : '下一题'} */}
             </Button>
             <Image
               src={iconButtonList}