Browse Source

添加评测详情

lex 1 year ago
parent
commit
cda8e3287c

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

@@ -52,6 +52,14 @@ export default [
         }
       },
       {
+        path: '/exercise-record',
+        name: 'exercise-record',
+        component: () => import('@/views/exercise-record/exercis-detail'),
+        meta: {
+          title: '评测详情'
+        }
+      },
+      {
         path: '/knowledge-library',
         name: 'knowledge-library',
         component: () => import('@/views/knowledge-library/index'),
@@ -111,6 +119,14 @@ export default [
         }
       },
       {
+        path: '/examination-mode',
+        name: 'examination-mode',
+        component: () => import('@/views/knowledge-library/examination-mode'),
+        meta: {
+          title: '模拟测试'
+        }
+      },
+      {
         path: '/courseware-list',
         name: 'courseware-list',
         component: () => import('@/views/courseware-list/index'),

+ 137 - 0
src/views/exercise-record/exercis-detail.module.less

@@ -0,0 +1,137 @@
+.exercisContainer {
+  background: url('./images/detail-bg.png') top center/ cover no-repeat;
+  background-size: contain;
+}
+
+.topWrap {
+  margin-bottom: 12px;
+
+  .topInfo {
+    padding: 34px 15px 30px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    flex-wrap: nowrap;
+
+    .topInfoLeft {
+      width: 50%;
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+
+      .headWrap {
+        flex-shrink: 0;
+        border-radius: 50%;
+        overflow: hidden;
+        border: 2px solid #fff;
+
+        margin-right: 15px;
+      }
+
+      .infoMsg {
+        p {
+          display: inline-block;
+          width: 120px;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          overflow: hidden;
+          font-size: 20px;
+          font-weight: 600;
+          color: #000000;
+          line-height: 28px;
+          margin-bottom: 6px;
+        }
+
+        .tag {
+          display: inline-block;
+          padding: 0px 11px;
+          border: 1px solid #B7E0FE;
+          background: #F5F5F5;
+          border-radius: 12px;
+          font-size: 12px;
+          font-weight: 500;
+          color: #1CACF1;
+          line-height: 22px;
+          text-align: center;
+        }
+      }
+    }
+
+    .topInfoRight {
+      width: 50%;
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: flex-end;
+
+      .infoDay {
+        margin-right: 30px;
+      }
+
+      .infoDayMain {
+        font-size: 25px;
+        color: #333333;
+        line-height: 28px;
+        margin-bottom: 7px;
+        font-family: 'DINA';
+        font-weight: 600;
+        text-align: center;
+
+        span {
+          margin-left: 2px;
+          font-size: 12px;
+          font-weight: 400;
+          color: #333333;
+          line-height: 17px;
+        }
+      }
+
+      .infoDaysub {
+        font-size: 12px;
+        font-weight: 400;
+        color: #333333;
+        line-height: 17px;
+        text-align: center;
+      }
+    }
+  }
+
+  .chioseWrap {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    // justify-content: space-around;
+    background-color: transparent;
+    font-size: 14px;
+    font-weight: 500;
+    color: #333333;
+    line-height: 20px;
+    font-size: 14px;
+  }
+}
+
+.select {
+  height: 45px;
+
+  .icon {
+    width: 18px;
+    height: 17px;
+    margin-right: 4px;
+    flex-shrink: 0;
+  }
+
+  :global {
+    .van-cell__title {
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+
+    .van-cell__right-icon {
+      color: #333;
+      transform: rotate(90deg);
+      margin: 0 0 0 4px;
+    }
+  }
+}

+ 240 - 0
src/views/exercise-record/exercis-detail.tsx

@@ -0,0 +1,240 @@
+import OHeader from '@/components/m-header';
+import OSticky from '@/components/m-sticky';
+import OEmpty from '@/components/m-empty';
+import dayjs from 'dayjs';
+import { DatePicker, Popup, List, Image, CellGroup, Cell } from 'vant';
+import OFullRefresh from '@/components/m-full-refresh';
+import DetailItem from './modals/detail-item';
+import { defineComponent, onMounted, reactive, ref, nextTick } from 'vue';
+import { useRoute } from 'vue-router';
+import styles from './exercis-detail.module.less';
+import request from '@/helpers/request';
+import iconStudent from '@common/images/icon-student.png';
+import iconData from './images/icon-data.png';
+import { useRect } from '@vant/use';
+import { formatterDatePicker } from '@/helpers/utils';
+import { useEventListener, useWindowScroll } from '@vueuse/core';
+import { state as baseState } from '@/state';
+
+export default defineComponent({
+  name: 'exercis-detail',
+  setup() {
+    const route = useRoute();
+    const state = reactive({
+      showPopoverTime: false,
+      currentDate: [dayjs().format('YYYY'), dayjs().format('MM')],
+      isClick: false,
+      background: 'transparent',
+      color: '#fff',
+      practiceMonthName: route.query.practiceMonthName
+        ? route.query.practiceMonthName
+        : dayjs().format('YYYY') + '年' + dayjs().format('MM') + '月'
+    });
+
+    const forms = reactive({
+      practiceMonth: route.query.practiceMonth
+        ? route.query.practiceMonth
+        : state.currentDate[0] + '' + state.currentDate[1],
+
+      page: 1,
+      rows: 20
+    });
+    const refreshing = ref(false);
+    const loading = ref(false);
+    const finished = ref(false);
+    const showContact = ref(false);
+    const infoDetail = ref({} as any);
+    const list = ref([]);
+    const getList = async () => {
+      if (state.isClick) {
+        return;
+      }
+      state.isClick = true;
+      if (refreshing.value) {
+        list.value = [];
+        forms.page = 1;
+        refreshing.value = false;
+      }
+      try {
+        const res = await request.post(`/edu-app/musicPracticeRecord/page`, {
+          data: { ...forms, feature: 'EVALUATION' }
+        });
+
+        if (list.value.length > 0 && res.data.current === 1) {
+          return;
+        }
+
+        list.value = list.value.concat(res.data.rows || []);
+        forms.page = res.data.current + 1;
+        showContact.value = list.value.length > 0;
+        loading.value = false;
+        finished.value = res.data.current >= res.data.pages;
+      } catch {
+        showContact.value = false;
+        finished.value = true;
+      }
+      state.isClick = false;
+    };
+
+    const getDetail = async () => {
+      try {
+        const res = await request.get(`/edu-app/student/detail`, {
+          params: {
+            id: baseState.user.data?.id
+          }
+        });
+        infoDetail.value = { ...res.data };
+      } catch (e: any) {}
+    };
+    const topWrap = ref();
+    const topWrapHeight = ref(0);
+    onMounted(async () => {
+      useEventListener(document, 'scroll', () => {
+        const { y } = useWindowScroll();
+        if (y.value > 52) {
+          state.background = '#fff';
+          state.color = '#323333';
+        } else {
+          state.background = 'transparent';
+          state.color = '#fff';
+        }
+      });
+
+      await getList();
+      await getDetail();
+
+      nextTick(() => {
+        const { height } = useRect(topWrap.value);
+        topWrapHeight.value = height;
+      });
+    });
+
+    const checkTimer = (val: any) => {
+      forms.practiceMonth = val.selectedValues[0] + val.selectedValues[1];
+      state.practiceMonthName =
+        val.selectedValues[0] + '年' + val.selectedValues[1] + '月';
+      state.showPopoverTime = false;
+      refreshing.value = true;
+      getList();
+    };
+
+    const onRefresh = () => {
+      finished.value = false;
+      // 重新加载数据
+      // 将 loading 设置为 true,表示处于加载状态
+      loading.value = true;
+      getList();
+    };
+
+    return () => (
+      <>
+        <div class={[styles.exercisContainer]}>
+          <div class={styles.topWrap} ref={topWrap}>
+            <OSticky position="top">
+              <OHeader
+                border={false}
+                background={state.background}
+                color={state.color}
+              />
+            </OSticky>
+            <div class={styles.topInfo}>
+              <div class={styles.topInfoLeft}>
+                <div class={styles.headWrap}>
+                  <Image
+                    src={
+                      infoDetail.value.avatar
+                        ? infoDetail.value.avatar
+                        : iconStudent
+                    }
+                    fit="cover"
+                    width="68px"
+                    height="68px"
+                  />
+                </div>
+                <div class={styles.infoMsg}>
+                  <p>{infoDetail.value.nickname}</p>
+                  <div class={styles.tag}>
+                    {infoDetail.value.subjectNames
+                      ? infoDetail.value.subjectNames
+                      : '暂无声部'}
+                  </div>
+                </div>
+              </div>
+              <div class={styles.topInfoRight}>
+                <div class={styles.infoDay}>
+                  <p class={styles.infoDayMain}>
+                    {infoDetail.value.practiceDays
+                      ? infoDetail.value.practiceDays
+                      : 0}
+                    <span>天</span>
+                  </p>
+                  <p class={styles.infoDaysub}>练习天数</p>
+                </div>
+                <div class={styles.infoTime}>
+                  <p class={styles.infoDayMain}>
+                    {infoDetail.value.practiceTimes
+                      ? infoDetail.value.practiceTimes
+                      : 0}
+                    <span>分钟</span>
+                  </p>
+                  <p class={styles.infoDaysub}>练习时长</p>
+                </div>
+              </div>
+            </div>
+            <CellGroup inset>
+              <Cell
+                class={styles.select}
+                center
+                isLink
+                onClick={() => (state.showPopoverTime = true)}>
+                {{
+                  icon: () => <img class={styles.icon} src={iconData} />,
+                  title: () => (
+                    <div class="van-ellipsis">{state.practiceMonthName}</div>
+                  )
+                }}
+              </Cell>
+            </CellGroup>
+          </div>
+          {showContact.value ? (
+            <OFullRefresh
+              v-model:modelValue={refreshing.value}
+              onRefresh={onRefresh}
+              style={{ minHeight: `calc(100vh - ${topWrapHeight.value}px)` }}>
+              <List
+                loading-text=" "
+                finished={finished.value}
+                finished-text=" "
+                onLoad={getList}>
+                {list.value.map((item: any) => (
+                  <DetailItem item={item} />
+                ))}
+              </List>
+            </OFullRefresh>
+          ) : (
+            <OEmpty
+              description="暂无练习统计"
+              style={{ height: `calc(100vh - ${topWrapHeight.value}px)` }}
+            />
+          )}
+        </div>
+
+        <Popup
+          v-model:show={state.showPopoverTime}
+          position="bottom"
+          round
+          class={'popupBottomSearch'}>
+          <DatePicker
+            onCancel={() => {
+              state.showPopoverTime = false;
+            }}
+            onConfirm={checkTimer}
+            v-model={state.currentDate}
+            formatter={formatterDatePicker}
+            columnsType={['year', 'month']}
+          />
+        </Popup>
+      </>
+    );
+  }
+});

BIN
src/views/exercise-record/images/Image1.png


BIN
src/views/exercise-record/images/Image2.png


BIN
src/views/exercise-record/images/Image3.png


BIN
src/views/exercise-record/images/Image4.png


BIN
src/views/exercise-record/images/Image5.png


BIN
src/views/exercise-record/images/detail-bg.png


BIN
src/views/exercise-record/images/good-icon.png


BIN
src/views/exercise-record/images/icon-data.png


BIN
src/views/exercise-record/images/icon-member.png


+ 94 - 0
src/views/exercise-record/modals/detail-item.module.less

@@ -0,0 +1,94 @@
+.itemWrap {
+  background: #ffffff;
+  border-radius: 10px;
+  padding: 12px 15px 20px;
+  margin: 0 13px 13px;
+
+  .itemTop {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 1px solid #f2f2f2;
+    padding-bottom: 12px;
+
+    .itemTopLeft {
+      .itemTopMain {
+        height: 22px;
+        font-size: 16px;
+        font-weight: 500;
+        color: #333333;
+        line-height: 22px;
+        margin-bottom: 6px;
+      }
+
+      .itemTopSub {
+        font-size: 12px !important;
+        font-weight: 400;
+        color: #777777;
+        line-height: 17px;
+      }
+    }
+
+    .itemTopRight {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+
+      .imgWrap {
+        width: 100px;
+        height: 33px;
+        background: #e9e3ff;
+        border-radius: 19px;
+
+        img {
+          width: 100%;
+          height: 100%;
+        }
+      }
+
+      .imgIcon {
+        font-size: 16px;
+        color: #d8d8d8;
+        margin-left: 6px;
+      }
+    }
+  }
+
+  .itemBottom {
+    margin-top: 15px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-around;
+    text-align: center;
+
+    .itemBottomDot {
+      width: 25%;
+
+      .dotMain {
+        font-size: 26px;
+        color: #333333;
+        line-height: 30px;
+        margin-bottom: 4px;
+        font-family: 'DINA';
+        font-weight: bold;
+
+        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;
+      }
+    }
+  }
+}

+ 124 - 0
src/views/exercise-record/modals/detail-item.tsx

@@ -0,0 +1,124 @@
+import { defineComponent } from 'vue';
+import styles from './detail-item.module.less';
+import { postMessage } from '@/helpers/native-message';
+import { Icon } from 'vant';
+import Image1 from '../images/Image1.png';
+import Image2 from '../images/Image2.png';
+import Image3 from '../images/Image3.png';
+import Image4 from '../images/Image4.png';
+import Image5 from '../images/Image5.png';
+
+const scoreInfos: any = {
+  1: {
+    img: Image1,
+    tips: '你的演奏不太好,音准和完整性还需加强,再练一练吧~',
+    mome: '敢于尝试'
+  },
+  2: {
+    img: Image2,
+    tips: '你的演奏还不熟练,音准和完整性还需加强,加紧训练才能有好成绩哦~',
+    mome: '还要加油哦~'
+  },
+  3: {
+    img: Image3,
+    tips: '你的演奏还不流畅,音准和节奏还需加强,科学的练习才能更完美哦~',
+    mome: '突破自我'
+  },
+  4: {
+    img: Image4,
+    tips: '你的演奏还不错,继续加油吧,加强音准,离完美就差一步啦~',
+    mome: '崭露头角'
+  },
+  5: {
+    img: Image5,
+    tips: '你的演奏非常不错,音准的把握和节奏稍有瑕疵,完整性把握的很好~',
+    mome: '你很棒'
+  }
+};
+export default defineComponent({
+  props: ['item'],
+  name: 'detail-item',
+
+  setup(props) {
+    const getLeveByScoreId = (score?: number) => {
+      if (!score && typeof score !== 'number') {
+        return {};
+      }
+      let leve: any = 1;
+      if (score > 20 && score <= 40) {
+        leve = 2;
+      } else if (score > 40 && score <= 60) {
+        leve = 3;
+      } else if (score > 60 && score <= 80) {
+        leve = 4;
+      } else if (score > 80) {
+        leve = 5;
+      }
+      return leve;
+    };
+    const gotoDetail = () => {
+      const url =
+        window.location.origin +
+        `/orchestra-music-score/report-share.html?id=${props.item.id}`;
+      postMessage({
+        api: 'openWebView',
+        content: {
+          url: url,
+          orientation: 0,
+          isHideTitle: true,
+          statusBarTextColor: false,
+          isOpenLight: true
+        }
+      });
+    };
+    return () => (
+      <div class={styles.itemWrap} onClick={gotoDetail}>
+        <div class={styles.itemTop}>
+          <div class={styles.itemTopLeft}>
+            <p class={styles.itemTopMain}>{props.item.musicSheetName}</p>
+            <p class={styles.itemTopSub}>{props.item.createTime}</p>
+          </div>
+          <div class={styles.itemTopRight}>
+            <div class={styles.imgWrap}>
+              <img
+                src={scoreInfos[getLeveByScoreId(props.item.score || 0)].img}
+                alt=""
+              />
+            </div>
+            <Icon name="arrow" class={styles.imgIcon} />
+          </div>
+        </div>
+        <div class={styles.itemBottom}>
+          <div class={styles.itemBottomDot}>
+            <p class={styles.dotMain} style={{ color: '#F67146' }}>
+              {props.item.score || 0}
+              <span>分</span>{' '}
+            </p>
+            <p class={styles.dotSub}> 综合得分</p>
+          </div>
+          <div class={styles.itemBottomDot}>
+            <p class={styles.dotMain}>
+              {props.item.intonation || 0}
+              <span>分</span>{' '}
+            </p>
+            <p class={styles.dotSub}>音准 </p>
+          </div>
+          <div class={styles.itemBottomDot}>
+            <p class={styles.dotMain}>
+              {props.item.cadence || 0}
+              <span>分</span>{' '}
+            </p>
+            <p class={styles.dotSub}>节奏 </p>
+          </div>
+          <div class={styles.itemBottomDot}>
+            <p class={styles.dotMain}>
+              {props.item.integrity || 0}
+              <span>分</span>{' '}
+            </p>
+            <p class={styles.dotSub}>完成度 </p>
+          </div>
+        </div>
+      </div>
+    );
+  }
+});

+ 103 - 0
src/views/knowledge-library/examination-mode/index.module.less

@@ -0,0 +1,103 @@
+.unitDetail {
+  min-height: 100vh;
+  overflow: hidden;
+  background: url('../images/bg.png') no-repeat top center;
+  background-size: contain;
+  position: relative;
+  background-color: #ABE8FF;
+}
+
+.unitSwipe {
+  margin-top: 75px;
+
+  .questionTitle {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    font-size: 14px;
+    font-weight: bold;
+    color: #333;
+    padding-bottom: 12px;
+
+    .questionNum {
+      span {
+        color: #FF5A56;
+      }
+    }
+
+    .questionType {
+      display: flex;
+      align-items: center;
+
+      span {
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        max-width: 160px;
+      }
+
+      i {
+        margin-right: 6px;
+        display: inline-block;
+        width: 20px;
+        height: 20px;
+        background: url('../images/icon-book.png') no-repeat center;
+        background-size: contain;
+      }
+    }
+  }
+}
+
+.wapList {
+  width: 44px;
+  height: 49px;
+  flex-shrink: 0;
+  margin-left: 18px;
+}
+
+.btnSection {
+  background-color: #fff;
+  padding-top: 20px;
+  --van-button-default-height: 49px;
+
+  .prevBtn,
+  .nextBtn,
+  .activePrevBtn {
+    width: 124px !important;
+    border: none;
+    color: #fff;
+  }
+
+  .prevBtn {
+    background: url('../images/prev_btn_bg.png') no-repeat center;
+    background-size: contain;
+  }
+
+  .activePrevBtn {
+    background: url('../images/next_btn_bg.png') no-repeat center;
+    background-size: contain;
+  }
+
+  .nextBtn {
+    background: url('../images/next_btn_bg.png') no-repeat center;
+    background-size: contain;
+  }
+
+  :global {
+    .van-button--disabled {
+      color: #587C98;
+
+      &:before {
+        opacity: 0.2 !important;
+      }
+    }
+  }
+}
+
+.right {
+  color: #14C295;
+}
+
+.error {
+  color: #FF5A56;
+}

+ 578 - 0
src/views/knowledge-library/examination-mode/index.tsx

@@ -0,0 +1,578 @@
+import { ActionSheet, Button, Image, Popup, Swipe, SwipeItem } from 'vant';
+import {
+  computed,
+  defineComponent,
+  nextTick,
+  onMounted,
+  onUnmounted,
+  reactive,
+  ref
+} from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import styles from './index.module.less';
+import iconButtonList from '../images/icon-button-list.png';
+import MSticky from '@/components/m-sticky';
+import ChoiceQuestion from '../model/choice-question';
+import AnswerList from '../model/answer-list';
+import DragQuestion from '../model/drag-question';
+import KeepLookQuestion from '../model/keep-look-question';
+import PlayQuestion from '../model/play-question';
+import ErrorMode from '../model/error-mode';
+import ResultFinish from '../model/result-finish';
+import { eventUnit, QuestionType } from '../unit';
+import request from '@/helpers/request';
+import { useRect } from '@vant/use';
+import MHeader from '@/components/m-header';
+import { useEventListener, useInterval, useWindowScroll } from '@vueuse/core';
+
+export default defineComponent({
+  name: 'unit-detail',
+  setup() {
+    const route = useRoute();
+    const router = useRouter();
+    const swipeRef = ref();
+    const state = reactive({
+      type: route.query.type, // 类型
+      knowledgePointIds: route.query.knowledgePointIds, // 智能组卷 多个编号
+      lessonCoursewareId: route.query.lessonCoursewareId, // 教材编号
+      background: 'transparent',
+      color: '#fff',
+      visiableError: false,
+      visiableAnswer: false,
+      id: route.query.id,
+      currentIndex: 0,
+      questionList: [],
+      visiableInfo: {
+        show: false,
+        operationType: 'RESULT' as 'RESULT' | 'BACK' | 'CONTINUE',
+        type: 'DEFAULT' as 'DEFAULT' | 'FAIL' | 'PASS' | 'GOOD' | 'COUNTDOWN',
+        content: '',
+        showCancelButton: false,
+        confirmButtonText: '',
+        cancelButtonText: '',
+        title: ''
+      },
+      nextStatus: false,
+      swipeHeight: 'auto' as any,
+      answerAnalysis: '',
+      questionTypeCode: '',
+      overResult: {
+        time: '00:00', // 时长
+        questionLength: 0, // 答题数
+        errorLength: 0, // 错题数
+        rate: 0 // 正确率
+      }
+    });
+
+    // 计时
+    const { counter, resume, pause } = useInterval(1000, { controls: true });
+
+    const getExamDetails = async () => {
+      try {
+        const { data } = await request.post(
+          '/edu-app/studentUnitExamination/queryPracticeModeExam',
+          {
+            data: {
+              lessonCoursewareId: state.lessonCoursewareId
+            }
+          }
+        );
+        const temp = data || {};
+        temp.examinationQuestionAdds.forEach((item: any) => {
+          item.showAnalysis = false; // 默认不显示解析
+          item.analysis = {
+            message: item.answerAnalysis,
+            topic: true, // 是否显示结果
+            userResult: false // 用户答题对错
+          };
+          item.userAnswer = []; // 用户答题
+        });
+        state.questionList = temp.examinationQuestionAdds || [];
+      } catch {
+        //
+      }
+    };
+
+    /**
+     * @description 下一题 | 测试完成
+     */
+    const onNextQuestion = async () => {
+      try {
+        const questionList = state.questionList || [];
+
+        let result: any = {};
+        questionList.forEach((question: any, index: number) => {
+          // 格式化所有题目的答案
+          if (index === state.currentIndex) {
+            result = {
+              questionId: question.id,
+              details: question.userAnswer || []
+            };
+          }
+        });
+
+        const { data } = await request.post(
+          '/edu-app/studentUnitExamination/submitTrainingAnswer',
+          {
+            hideLoading: true,
+            data: result
+          }
+        );
+        // 初始化是否显示解析
+        questionList.forEach((question: any, index: number) => {
+          // 格式化所有题目的答案
+          if (index === state.currentIndex) {
+            state.answerAnalysis = question.answerAnalysis;
+            state.questionTypeCode = question.questionTypeCode;
+            question.showAnalysis = true;
+            question.analysis.userResult = data;
+          }
+        });
+
+        // 判断是否是最后一题
+        if (state.questionList.length === state.currentIndex + 1) {
+          eventUnit.emit('unitAudioStop');
+          state.visiableInfo.show = true;
+          state.visiableInfo.title = '练习完成';
+          state.visiableInfo.showCancelButton = true;
+          state.visiableInfo.operationType = 'CONTINUE';
+          state.visiableInfo.cancelButtonText = '再等等';
+          state.visiableInfo.confirmButtonText = '确认完成';
+          state.visiableInfo.content = `确认本次练习的题目都完成了吗?`;
+
+          return;
+        }
+
+        if (data) {
+          swipeRef.value?.next();
+        } else {
+          state.visiableError = true;
+        }
+      } catch {
+        //
+      }
+    };
+
+    //
+    const getAnswerResult = computed(() => {
+      const questionList = state.questionList || [];
+      let count = 0;
+      let passCount = 0;
+      let noPassCount = 0;
+      questionList.forEach((item: any) => {
+        if (item.showAnalysis) {
+          count += 1;
+          if (item.analysis.userResult) {
+            passCount += 1;
+          } else {
+            noPassCount += 1;
+          }
+        }
+      });
+
+      return {
+        count,
+        passCount,
+        noPassCount
+      };
+    });
+
+    /**
+     * @description 重置当前的题目高度
+     * @param {any} scroll 是否滚动到顶部
+     */
+    let size = 0;
+    const resizeSwipeItemHeight = (scroll = true) => {
+      nextTick(() => {
+        scroll && window.scrollTo(0, 0);
+        setTimeout(() => {
+          const currentItemDom: any = document
+            .querySelectorAll('.van-swipe-item')
+            [state.currentIndex]?.querySelector('.swipe-item-question');
+
+          const allImg = currentItemDom?.querySelectorAll(
+            '.answerTitleImg img'
+          );
+          let status = true;
+          // console.log(allImg)
+          allImg?.forEach((img: any) => {
+            console.log(img.complete);
+            if (!img.complete) {
+              status = false;
+            }
+          });
+          // 判断图片是否加载完了
+          if (!status && size < 3) {
+            setTimeout(() => {
+              size += 1;
+              resizeSwipeItemHeight(scroll);
+            }, 300);
+          }
+          if (status) {
+            size = 0;
+          }
+          const rect = useRect(currentItemDom);
+          state.swipeHeight = rect.height;
+        }, 100);
+      });
+    };
+
+    const onConfirmExam = () => {
+      //
+    };
+
+    const onConfirmResult = () => {
+      if (state.visiableInfo.operationType === 'RESULT') {
+        state.visiableInfo.show = false;
+        router.back();
+        onAfter();
+      } else if (state.visiableInfo.operationType === 'BACK') {
+        state.visiableInfo.show = false;
+        onAfter();
+      } else if (state.visiableInfo.operationType === 'CONTINUE') {
+        onResultPopup();
+      }
+    };
+    const onCloseResult = async () => {
+      if (state.visiableInfo.operationType === 'RESULT') {
+      } else if (state.visiableInfo.operationType === 'BACK') {
+        state.visiableInfo.show = false;
+        window.history.pushState(null, '', document.URL);
+        window.addEventListener('popstate', onBack, false);
+      } else if (state.visiableInfo.operationType === 'CONTINUE') {
+        state.visiableInfo.show = false;
+      }
+    };
+
+    /** 结果页面弹窗 */
+    const onResultPopup = () => {
+      const answerResult = getAnswerResult.value;
+      let rate = 0;
+
+      if (answerResult.count > 0) {
+        rate = Math.floor((answerResult.passCount / answerResult.count) * 100);
+      }
+
+      const times = counter.value;
+      const minute =
+        Math.floor(times / 60) >= 10
+          ? Math.floor(times / 60)
+          : '0' + Math.floor(times / 60);
+      const seconds = times % 60 >= 10 ? times % 60 : '0' + (times % 60);
+      state.overResult = {
+        time: minute + ':' + seconds, // 时长
+        questionLength: answerResult.count, // 答题数
+        errorLength: answerResult.noPassCount, // 错题数
+        rate // 正确率
+      };
+      // 重置计时
+      pause();
+      counter.value = 0;
+
+      // 60 及格
+      // 85 及以上优秀
+      state.visiableInfo.show = true;
+      state.visiableInfo.title = '已完成';
+      state.visiableInfo.showCancelButton = false;
+      state.visiableInfo.operationType = 'RESULT';
+      state.visiableInfo.confirmButtonText = '确认';
+      state.visiableInfo.content = `<div>您已完成本次测试,答对<span class='${
+        styles.right
+      }'>${answerResult.passCount}</span>,答错<span class='${styles.error}'>${
+        answerResult.count - answerResult.passCount
+      }</span>,正确率${rate}%~</div>`;
+    };
+
+    // 拦截
+    const onBack = () => {
+      const answerResult = getAnswerResult.value;
+      state.visiableInfo.show = true;
+      state.visiableInfo.title = '确认退出吗?';
+      state.visiableInfo.showCancelButton = true;
+      state.visiableInfo.operationType = 'BACK';
+      state.visiableInfo.cancelButtonText = '取消';
+      state.visiableInfo.confirmButtonText = '确定';
+      state.visiableInfo.content = `您已经完成${
+        answerResult.passCount + answerResult.noPassCount
+      }道题了,继续做题可以巩固所学知识哦~`;
+      eventUnit.emit('unitAudioStop');
+    };
+
+    const onAfter = () => {
+      window.removeEventListener('popstate', onBack, false);
+      router.back();
+    };
+
+    onMounted(async () => {
+      useEventListener(document, 'scroll', () => {
+        const { y } = useWindowScroll();
+        if (y.value > 52) {
+          state.background = '#fff';
+          state.color = '#323333';
+        } else {
+          state.background = 'transparent';
+          state.color = '#fff';
+        }
+      });
+      await getExamDetails();
+
+      resizeSwipeItemHeight();
+
+      window.history.pushState(null, '', document.URL);
+      window.addEventListener('popstate', onBack, false);
+    });
+
+    onUnmounted(() => {
+      // 关闭所有音频
+      eventUnit.emit('unitAudioStop');
+    });
+    return () => (
+      <div class={styles.unitDetail}>
+        <MSticky position="top">
+          <MHeader
+            border={false}
+            background={state.background}
+            color={state.color}
+          />
+        </MSticky>
+        <Swipe
+          loop={false}
+          showIndicators={false}
+          ref={swipeRef}
+          duration={300}
+          touchable={false}
+          class={styles.unitSwipe}
+          style={{ paddingBottom: '12px' }}
+          lazyRender
+          height={state.swipeHeight}
+          onChange={(index: number) => {
+            eventUnit.emit('unitAudioStop');
+            state.currentIndex = index;
+            resizeSwipeItemHeight();
+          }}>
+          {state.questionList.map((item: any, index: number) => (
+            <SwipeItem>
+              <div class="swipe-item-question">
+                {item.questionTypeCode === QuestionType.RADIO && (
+                  <ChoiceQuestion
+                    v-model:value={item.userAnswer}
+                    index={index + 1}
+                    data={item}
+                    type="radio"
+                    showAnalysis={item.showAnalysis}
+                    analysis={item.analysis}>
+                    {{
+                      title: () => (
+                        <div class={styles.questionTitle}>
+                          <div class={styles.questionNum}>
+                            <span>{state.currentIndex + 1}</span>/
+                            {state.questionList.length}
+                          </div>
+                          <div class={styles.questionType}>
+                            <i></i>
+                            <span>{item.knowledgePointName}</span>
+                          </div>
+                        </div>
+                      )
+                    }}
+                  </ChoiceQuestion>
+                )}
+                {item.questionTypeCode === QuestionType.CHECKBOX && (
+                  <ChoiceQuestion
+                    v-model:value={item.userAnswer}
+                    index={index + 1}
+                    data={item}
+                    type="checkbox"
+                    showAnalysis={item.showAnalysis}
+                    analysis={item.analysis}>
+                    {{
+                      title: () => (
+                        <div class={styles.questionTitle}>
+                          <div class={styles.questionNum}>
+                            <span>{state.currentIndex + 1}</span>/
+                            {state.questionList.length}
+                          </div>
+                          <div class={styles.questionType}>
+                            <i></i>
+                            <span>{item.knowledgePointName}</span>
+                          </div>
+                        </div>
+                      )
+                    }}
+                  </ChoiceQuestion>
+                )}
+                {item.questionTypeCode === QuestionType.SORT && (
+                  <DragQuestion
+                    v-model:value={item.userAnswer}
+                    onUpdate:value={() => {
+                      // 如果是空则滑动到顶部
+                      const status =
+                        item.userAnswer && item.userAnswer.length > 0
+                          ? false
+                          : true;
+                      resizeSwipeItemHeight(status);
+                    }}
+                    data={item}
+                    index={index + 1}
+                    showAnalysis={item.showAnalysis}
+                    analysis={item.analysis}>
+                    {{
+                      title: () => (
+                        <div class={styles.questionTitle}>
+                          <div class={styles.questionNum}>
+                            <span>{state.currentIndex + 1}</span>/
+                            {state.questionList.length}
+                          </div>
+                          <div class={styles.questionType}>
+                            <i></i>
+                            <span>{item.knowledgePointName}</span>
+                          </div>
+                        </div>
+                      )
+                    }}
+                  </DragQuestion>
+                )}
+                {item.questionTypeCode === QuestionType.LINK && (
+                  <KeepLookQuestion
+                    v-model:value={item.userAnswer}
+                    data={item}
+                    index={index + 1}
+                    showAnalysis={item.showAnalysis}
+                    analysis={item.analysis}>
+                    {{
+                      title: () => (
+                        <div class={styles.questionTitle}>
+                          <div class={styles.questionNum}>
+                            <span>{state.currentIndex + 1}</span>/
+                            {state.questionList.length}
+                          </div>
+                          <div class={styles.questionType}>
+                            <i></i>
+                            <span>{item.knowledgePointName}</span>
+                          </div>
+                        </div>
+                      )
+                    }}
+                  </KeepLookQuestion>
+                )}
+                {item.questionTypeCode === QuestionType.PLAY && (
+                  <PlayQuestion
+                    v-model:value={item.userAnswer}
+                    data={item}
+                    index={index + 1}
+                    unitId={state.id as any}
+                    showAnalysis={item.showAnalysis}
+                    analysis={item.analysis}>
+                    {{
+                      title: () => (
+                        <div class={styles.questionTitle}>
+                          <div class={styles.questionNum}>
+                            <span>{state.currentIndex + 1}</span>/
+                            {state.questionList.length}
+                          </div>
+                          <div class={styles.questionType}>
+                            <i></i>
+                            <span>{item.knowledgePointName}</span>
+                          </div>
+                        </div>
+                      )
+                    }}
+                  </PlayQuestion>
+                )}
+              </div>
+            </SwipeItem>
+          ))}
+        </Swipe>
+
+        <MSticky position="bottom">
+          <div class={['btnGroup btnMore', styles.btnSection]}>
+            <Button
+              round
+              block
+              class={
+                state.currentIndex > 0 ? styles.activePrevBtn : styles.prevBtn
+              }
+              disabled={state.currentIndex > 0 ? false : true}
+              onClick={() => {
+                swipeRef.value?.prev();
+              }}>
+              上一题
+            </Button>
+            <Button
+              block
+              round
+              class={styles.nextBtn}
+              onClick={onNextQuestion}
+              loading={state.nextStatus}
+              disabled={state.nextStatus}>
+              {state.questionList.length === state.currentIndex + 1
+                ? '提交'
+                : '下一题'}
+            </Button>
+            <Image
+              src={iconButtonList}
+              class={[styles.wapList, 'van-haptics-feedback']}
+              onClick={() => (state.visiableAnswer = true)}
+            />
+          </div>
+        </MSticky>
+
+        {/* 题目集合 */}
+        <ActionSheet
+          v-model:show={state.visiableAnswer}
+          title="题目列表"
+          safeAreaInsetBottom>
+          <AnswerList
+            value={state.questionList}
+            // lookType={'ANSWER'}
+            onSelect={(item: any) => {
+              // 跳转,并且跳过动画
+              swipeRef.value?.swipeTo(item, {
+                immediate: true
+              });
+              state.visiableAnswer = false;
+            }}
+          />
+        </ActionSheet>
+
+        <Popup
+          v-model:show={state.visiableError}
+          style={{ width: '90%' }}
+          round
+          closeOnClickOverlay={false}>
+          <ErrorMode
+            onClose={() => (state.visiableError = false)}
+            answerAnalysis={state.answerAnalysis}
+            questionTypeCode={state.questionTypeCode}
+            onConform={() => {
+              swipeRef.value?.next();
+              state.answerAnalysis = '';
+            }}
+          />
+        </Popup>
+
+        <Popup
+          v-model:show={state.visiableInfo.show}
+          closeOnClickOverlay={false}
+          style={{
+            background: 'transparent',
+            width: '100%',
+            maxWidth: '100%',
+            transform: 'translateY(-55%)'
+          }}>
+          <ResultFinish
+            title={state.visiableInfo.title}
+            showCancelButton={state.visiableInfo.showCancelButton}
+            cancelButtonText={state.visiableInfo.cancelButtonText}
+            confirmButtonText={state.visiableInfo.confirmButtonText}
+            status={state.visiableInfo.type}
+            content={state.visiableInfo.content}
+            contentHtml
+            onConform={onConfirmResult}
+            onClose={onCloseResult}
+          />
+        </Popup>
+      </div>
+    );
+  }
+});

+ 1 - 0
src/views/knowledge-library/index.module.less

@@ -73,6 +73,7 @@
     margin-top: 60px;
 
     .woringContent {
+      min-height: calc(100vh - var(--header-height) - 90px);
       border-radius: 20px;
       overflow: hidden;
       background-color: #fff;

+ 37 - 7
src/views/knowledge-library/index.tsx

@@ -54,6 +54,38 @@ export default defineComponent({
       }
     };
 
+    // 练习模式
+    const onGotoModel = async () => {
+      try {
+        await request.post(
+          '/edu-app/studentUnitExamination/checkKnowledgePointIds',
+          {
+            data: {
+              lessonCoursewareId: forms.cid
+            }
+          }
+        );
+        router.push({
+          path: '/practice-mode',
+          query: { lessonCoursewareId: route.query.detailId }
+        });
+      } catch {
+        //
+      }
+    };
+
+    // 模拟测试
+    const onExaminatoinModel = async () => {
+      try {
+        router.push({
+          path: '/examination-mode',
+          query: { lessonCoursewareId: route.query.detailId }
+        });
+      } catch {
+        //
+      }
+    };
+
     onMounted(() => {
       useEventListener(document, 'scroll', () => {
         const { y } = useWindowScroll();
@@ -90,13 +122,11 @@ export default defineComponent({
           <Button
             class={styles.btnPractice}
             round
-            onClick={() =>
-              router.push({
-                path: '/practice-mode',
-                query: { lessonCoursewareId: forms.cid }
-              })
-            }></Button>
-          <Button class={styles.btnTest} round></Button>
+            onClick={onGotoModel}></Button>
+          <Button
+            class={styles.btnTest}
+            round
+            onClick={onExaminatoinModel}></Button>
         </div>
 
         <div class={[styles.containerSection, styles.librarySection]}>

+ 1 - 0
src/views/knowledge-library/practice-mode/index.tsx

@@ -224,6 +224,7 @@ export default defineComponent({
     const onConfirmResult = () => {
       if (state.visiableInfo.operationType === 'RESULT') {
         state.visiableInfo.show = false;
+        router.back();
         onAfter();
       } else if (state.visiableInfo.operationType === 'BACK') {
         state.visiableInfo.show = false;

+ 30 - 3
src/views/knowledge-library/unit-detail.tsx

@@ -16,12 +16,14 @@ export default defineComponent({
 
     const forms = reactive({
       detailId: route.query.detailId,
+      loading: false,
       background: 'transparent',
       color: '#fff',
       dataInfo: {} as any
     });
 
     const getList = async () => {
+      forms.loading = true;
       try {
         const { data } = await request.get(
           '/edu-app/lessonCoursewareKnowledgeDetail/detail/' + forms.detailId
@@ -30,6 +32,26 @@ export default defineComponent({
       } catch {
         //
       }
+      forms.loading = false;
+    };
+
+    const onGotoModel = async () => {
+      try {
+        await request.post(
+          '/edu-app/studentUnitExamination/checkKnowledgePointIds',
+          {
+            data: {
+              lessonCoursewareId: forms.detailId
+            }
+          }
+        );
+        router.push({
+          path: '/practice-mode',
+          query: { lessonCoursewareId: route.query.detailId }
+        });
+      } catch {
+        //
+      }
     };
 
     onMounted(() => {
@@ -66,18 +88,23 @@ export default defineComponent({
         </MSticky>
 
         <div class={[styles.containerSection, styles.woringSection]}>
-          {forms.dataInfo.desc ? (
+          {forms.dataInfo.desc && (
             <div
               class={styles.woringContent}
               v-html={forms.dataInfo.desc}></div>
-          ) : (
+          )}
+          {!forms.dataInfo.desc && !forms.loading && (
             <div class={styles.woringContent}>
               <MEmpty description="暂无内容" style={{ paddingTop: '40px' }} />
             </div>
           )}
         </div>
 
-        <img src={iconExamQuestion} class={styles.iconExamQuestion} />
+        <img
+          src={iconExamQuestion}
+          class={styles.iconExamQuestion}
+          onClick={onGotoModel}
+        />
       </div>
     );
   }

+ 7 - 8
src/views/knowledge-library/wroing-book/ai-exam/index.tsx

@@ -48,14 +48,13 @@ export default defineComponent({
           showToast('请选择练习知识点');
           return;
         }
-
-        // router.push({
-        //   path: '/practice-mode',
-        //   query: {
-        //     type: 'ai',
-        //     knowledgePointIds: forms.checked.join(',')
-        //   }
-        // });
+        router.push({
+          path: '/examination-mode',
+          query: {
+            type: 'ai',
+            knowledgePointIds: forms.checked.join(',')
+          }
+        });
       } catch {
         //
       }