Parcourir la source

Merge branch 'mp' of http://49.234.186.218:9000/root/ieplus-app into mp

abpcoder il y a 22 heures
Parent
commit
cd9fdd4e1e

+ 40 - 4
src/api/modules/study.ts

@@ -1,6 +1,6 @@
 import { ApiResponse, ApiResponseList } from "@/types";
 import flyio from "../flyio";
-import { Batch, ClassKnowledgeRecord, DirectedSchool, Examinee, ExamPaper, ExamPaperSubmit, GetExamPaperRequestDTO, Knowledge, KnowledgeListRequestDTO, KnowledgeRecord, OpenExamineeRequestDTO, PaperWork, PaperWorkRecord, PaperWorkRecordDetail, PaperWorkRecordQuery, PracticeHistory, PracticeRecord, SimulatedRecord, SimulationExamSubject, SimulationTestInfo, StudentExamRecord, StudentPlanStudyRecord, StudentVideoRecord, StudyPlan, Subject, SubjectListRequestDTO, TeachClass, VideoStudy } from "@/types/study";
+import type { Batch, ClassKnowledgeRecord, DirectedSchool, Examinee, ExamPaper, ExamPaperSubmit, FavoriteQuestion, FavoriteQuestionListRequestDTO, GetExamPaperRequestDTO, Knowledge, KnowledgeListRequestDTO, KnowledgeRecord, OpenExamineeRequestDTO, PaperWork, PaperWorkRecord, PaperWorkRecordDetail, PaperWorkRecordQuery, PracticeHistory, PracticeRecord, SimulatedRecord, SimulationExamSubject, SimulationTestInfo, StudentExamRecord, StudentPlanStudyRecord, StudentSubject, StudentVideoRecord, StudyPlan, Subject, SubjectListRequestDTO, TeachClass, VideoStudy, WrongBookQuestion, WrongBookQuestionRequestDTO } from "@/types/study";
 import { EnumPaperWorkState } from "@/common/enum";
 
 /**
@@ -213,7 +213,7 @@ export function correctQuestion(params: { questionid: number, remark: string })
  * @param params 
  * @returns 
  */
-export function getPracticeHistory({pageNum, pageSize}: {pageNum: number, pageSize: number}) {
+export function getPracticeHistory({ pageNum, pageSize }: { pageNum: number, pageSize: number }) {
   return flyio.get('/front/student/record/practice', {
     pageNum,
     pageSize
@@ -229,7 +229,7 @@ export function getTextbooksPracticeHistory() {
   return flyio.get('/front/student/record/coursePractice', {}) as Promise<ApiResponseList<PracticeHistory>>;
 }
 
-export function getPaperWorkList(parmas: {state?: EnumPaperWorkState}) {
+export function getPaperWorkList(parmas: { state?: EnumPaperWorkState }) {
   return flyio.get('/front/student/record/test', parmas) as Promise<ApiResponseList<PaperWork>>;
 }
 
@@ -313,4 +313,40 @@ export function getTeacherTestRecordDetail(params: any) {
 
 export function getTeacherTestRecordCondition(params: any) {
   return flyio.get('/front/teacher/record/test/cond', params) as Promise<ApiResponse<PaperWorkRecordQuery>>;
-}
+}
+
+/**
+ * 获取收藏题目列表
+ * @param params 
+ * @returns 
+ */
+export function getFavoriteQuestionList(params: FavoriteQuestionListRequestDTO) {
+  return flyio.get('/front/favorites/questions', params) as Promise<ApiResponseList<FavoriteQuestion>>;
+}
+
+/**
+ * 获取学生科目
+ * @param params 
+ * @returns 
+ */
+export function getStudentSubject() {
+  return flyio.get('/front/student/subject') as Promise<ApiResponse<StudentSubject[]>>;
+}
+
+/**
+ * 获取错题本列表
+ * @param params 
+ * @returns 
+ */
+export function getWrongBookList(params: WrongBookQuestionRequestDTO) {
+  return flyio.get('/front/v2/wrongBook/wrongQuestions', params) as Promise<ApiResponseList<WrongBookQuestion>>;
+}
+
+/**
+ * 删除错题
+ * @param params 
+ * @returns 
+ */
+export function deleteWrongQuestion(questionId: number) {
+  return flyio.post('/front/v2/wrongBook/deleteWrongQuestion', null, { params: { questionId } }) as Promise<ApiResponse<any>>;
+}

+ 5 - 1
src/common/routes.ts

@@ -103,7 +103,7 @@ export const routes = {
   /**
    * 错题本
    */
-  pageWrongBook: '/pagesOther/pages/topic-center/wrong-book/wrong-book',
+  pageWrongBook: '/pagesStudy/pages/wrong-book/wrong-book',
   /**
    * 学习记录
    */
@@ -112,6 +112,10 @@ export const routes = {
    * 会员卡校验
    */
   pageCardVerify: '/pagesSystem/pages/card-verify/card-verify',
+  /**
+   * 试题收藏夹
+   */
+  pageQuestionFavorites: '/pagesStudy/pages/question-favorites/question-favorites',
 
 } as const;
 

+ 134 - 0
src/composables/useQuestionBook.ts

@@ -0,0 +1,134 @@
+import { cancelCollectQuestion, collectQuestion, deleteWrongQuestion as deleteWrongQuestionApi } from "@/api/modules/study";
+import { EnumQuestionType } from "@/common/enum";
+import { Study } from "@/types";
+
+const useQuestionBook = () => {
+  /**
+   * 题目是否正确
+   * @param qs 题目
+   * @returns 是否正确
+   */
+  const isQuestionCorrect = (qs: Study.FavoriteQuestionVO): boolean => {
+    let { answers, answer, typeId } = qs;
+    answers = answers?.filter(item => !!item) || [];
+    answer = answer || '';
+    if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(typeId)) {
+      return answer.includes(answers[0]);
+    } else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(typeId)) {
+      return answers.length === answer.length && answers.every(item => answer.includes(item));
+    } else {
+      // 主观题 A 对 B 错
+      return answers.includes('A') && !answers.includes('B');
+    }
+  };
+  /**
+   * 题目是否未作答
+   * @param qs 题目
+   * @returns 是否未作答
+   */
+  const isQuestionNotAnswer = (qs: Study.FavoriteQuestionVO): boolean => {
+    return !qs.answers || qs.answers.filter(item => !!item).length === 0;
+  }
+  /**
+   * 选项是否正确
+   * @param qs 题目
+   * @param option 选项
+   * @returns 是否正确
+   */
+  const isOptionCorrect = (qs: Study.FavoriteQuestionVO, option: Study.QuestionOption) => {
+    const { answers, answer, typeId } = qs;
+    if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(typeId)) {
+      return answer?.includes(option.no);
+    } else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(typeId)) {
+      return answer?.includes(option.no);
+    } else {
+      return answers?.includes(option.no) && option.no === 'A';
+    }
+  }
+  /**
+   * 选项是否选中
+   * @param qs 题目
+   * @param option 选项
+   * @returns 是否选中
+   */
+  const isOptionSelected = (qs: Study.FavoriteQuestionVO, option: Study.QuestionOption) => {
+    return qs.answers.includes(option.no);
+  }
+  /**
+   * 取消收藏
+   * @param id 题目ID
+   * @returns 是否成功
+   */
+  const cancelCollect = (id: number): Promise<boolean> => {
+    return new Promise((resolve, reject) => {
+      uni.$ie.showConfirm({
+        title: '提示',
+        content: '确定取消收藏吗?',
+      }).then(async confirm => {
+        if (confirm) {
+          try {
+            await cancelCollectQuestion(id);
+            uni.$ie.showToast('取消收藏成功');
+            resolve(true);
+          } catch (error) {
+            uni.$ie.showToast('取消收藏失败');
+            reject(error);
+          }
+        } else {
+          reject(new Error('用户取消'));
+        }
+      });
+    });
+  }
+  /**
+   * 收藏题目
+   * @param id 题目ID
+   * @returns 是否成功
+   */
+  const collect = (id: number): Promise<boolean> => {
+    return new Promise(async (resolve, reject) => {
+      try {
+        await collectQuestion(id);
+        uni.$ie.showToast('收藏成功');
+        resolve(true);
+      } catch (error) {
+        uni.$ie.showToast('收藏失败');
+        reject(error);
+      }
+    });
+  }
+  const deleteWrongQuestion = (id: number): Promise<boolean> => {
+    return new Promise(async (resolve, reject) => {
+      uni.$ie.showConfirm({
+        title: '提示',
+        content: '确定删除错题吗?',
+      }).then(async confirm => {
+        if (confirm) {
+          try {
+            await deleteWrongQuestionApi(id);
+            uni.$ie.showToast('删除错题成功');
+            resolve(true);
+          } catch (error) {
+            uni.$ie.showToast('删除错题失败');
+            reject(error);
+          }
+        } else {
+          reject(new Error('用户取消'));
+        }
+      });
+    });
+  }
+
+
+  return {
+    isQuestionCorrect,
+    isQuestionNotAnswer,
+    isOptionCorrect,
+    isOptionSelected,
+    cancelCollect,
+    collect,
+    deleteWrongQuestion
+  }
+}
+
+export default useQuestionBook;

+ 18 - 0
src/pages.json

@@ -224,6 +224,12 @@
           "style": {
             "navigationBarTitleText": ""
           }
+        },
+        {
+          "path": "pages/startup/startup",
+          "style": {
+            "navigationBarTitleText": ""
+          }
         }
       ]
     },
@@ -369,6 +375,18 @@
           "style": {
             "navigationBarTitleText": ""
           }
+        },
+        {
+          "path": "pages/wrong-book/wrong-book",
+          "style": {
+            "navigationBarTitleText": ""
+          }
+        },
+        {
+          "path": "pages/question-favorites/question-favorites",
+          "style": {
+            "navigationBarTitleText": ""
+          }
         }
       ]
     }

+ 2 - 2
src/pagesMain/pages/index/components/index-banner.vue

@@ -17,8 +17,8 @@
 import { useTransferPage } from '@/hooks/useTransferPage';
 const { transferTo, routes } = useTransferPage();
 import { useUserStore } from '@/store/userStore';
-import { Transfer } from '@/types';
-import {routes} from "@/common/routes";
+import type { Transfer } from '@/types';
+
 const userStore = useUserStore();
 type MenuItem = {
   name: string;

+ 1 - 1
src/pagesOther/pages/collect/components/collect-major.vue

@@ -1,6 +1,6 @@
 <template>
   <z-paging ref="paging" :auto="false" v-model="list" :safe-area-inset-bottom="true" :hide-no-more-by-limit="10" @query="handleQuery">
-    <uv-cell-group>
+    <uv-cell-group :border="false">
       <uv-cell v-for="item in list" :key="item.id" :title="item.name" :label="item.code" @click="handleClick(item)">
         <template #title>
           <text class="text-30 text-fore-title">{{ item.name }}</text>

+ 245 - 0
src/pagesStudy/components/question-book-item.vue

@@ -0,0 +1,245 @@
+<template>
+  <view class="question-book-item p-24">
+    <view class="question-title">
+      <mp-html :content="title" />
+    </view>
+    <view class="question-options">
+      <view class="question-option" v-for="(option, index) in data.options" :class="getStyleClass(option)" :key="index">
+        <view>
+          <uv-icon v-if="option.isCorrect" name="checkmark-circle-fill" color="#2CC6A0" size="22" />
+          <uv-icon v-else-if="!option.isCorrect && option.isSelected" name="close-circle-fill" color="#FF5B5C"
+            size="22" />
+          <view v-else class="question-option-index">{{ option.no }}</view>
+        </view>
+        <view class="question-option-content">
+          <mp-html :content="option.name" />
+        </view>
+      </view>
+    </view>
+    <uv-line margin="15px 0 10px 0" />
+    <view class="question-toolbar">
+      <view class="flex items-center gap-6" @click="handleToggleCollect">
+        <uv-icon :name="data.collect ? 'star-fill' : 'star'" size="20" color="var(--primary-color)" />
+        <text class="text-26 text-primary">{{ data.collect ? '已收藏' : '收藏' }}</text>
+      </view>
+      <view class="flex items-center gap-6" @click="handleCorrect">
+        <uv-icon name="info-circle" size="18" color="var(--primary-color)" />
+        <text class="text-26 text-primary">纠错</text>
+      </view>
+      <view v-if="!showAnswer" class="flex items-center" @click="toggleShowParse">
+        <cover-view class="w-60 h-60 flex items-center justify-center">
+          <cover-image v-show="!showParse" src="@/pagesSystem/static/image/icon/icon-eye.png" mode="widthFix"
+            class="w-38 h-38" />
+          <cover-image v-show="showParse" src="@/pagesSystem/static/image/icon/icon-eye-off.png" mode="widthFix"
+            class="w-38 h-38" />
+        </cover-view>
+        <text class="text-26 text-primary">{{ showParse ? '隐藏解析' : '查看解析' }}</text>
+      </view>
+      <view v-else class="flex items-center" @click="handleDelete">
+        <uv-icon name="trash" size="18" color="var(--danger)" />
+        <text class="text-26 text-danger">删除</text>
+      </view>
+    </view>
+    <view v-show="showParse" class="question-parse">
+      <view v-if="showAnswer">
+        <view class="text-28 text-fore-title font-bold">知识点</view>
+        <view class="mt-10 text-26 text-primary">
+          <mp-html :content="data.knowledge || '暂无知识点'" />
+        </view>
+      </view>
+      <view>
+        <view class="text-28 text-fore-title font-bold">答案</view>
+        <view class="mt-10 text-28 text-primary">
+          <mp-html :content="data.answer || '略'" />
+        </view>
+      </view>
+      <view>
+        <view class="text-28 text-fore-title font-bold">解析</view>
+        <view class="mt-10 text-26 text-primary">
+          <mp-html :content="data.parse || '暂无解析'" />
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+import { EnumQuestionType } from '@/common/enum';
+import type { Study } from '@/types';
+
+
+const props = defineProps({
+  showKnowledge: {
+    type: Boolean,
+    default: false
+  },
+  showAnswer: {
+    type: Boolean,
+    default: false
+  },
+  data: {
+    type: Object as PropType<Study.FavoriteQuestionVO>,
+    default: () => ({})
+  }
+});
+const showParse = ref(props.showAnswer);
+const title = computed(() => {
+  return `[${props.data.qtype}] ${props.data.title}`;
+});
+
+const emit = defineEmits<{
+  (e: 'correct', question: Study.FavoriteQuestionVO): void;
+  (e: 'toggleCollect', question: Study.FavoriteQuestionVO): void;
+  (e: 'delete', question: Study.FavoriteQuestionVO): void;
+}>();
+const handleCorrect = () => {
+  emit('correct', props.data);
+}
+const handleToggleCollect = () => {
+  emit('toggleCollect', props.data);
+}
+const handleDelete = () => {
+  emit('delete', props.data);
+}
+const toggleShowParse = () => {
+  showParse.value = !showParse.value;
+}
+const getStyleClass = (option: Study.QuestionOption) => {
+  let customClass = '';
+  let { answers, answer, typeId } = props.data;
+  answers = answers?.filter(item => item !== ' ') || [];
+  answer = answer || ''
+  if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(typeId)) {
+    if (option.isCorrect) {
+      customClass = 'question-option-correct';
+    } else if (option.isIncorrect) {
+      customClass = 'question-option-incorrect';
+    }
+  } else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(typeId)) {
+    // 我选择的答案
+    if (option.isSelected) {
+      if (option.isCorrect) {
+        customClass = 'question-option-correct';
+      } else {
+        customClass = 'question-option-incorrect';
+      }
+    } else {
+      // 漏选的答案
+      if (option.isMissed) {
+        customClass = 'question-option-miss';
+      }
+    }
+  }
+  return customClass;
+};
+</script>
+<style lang="scss" scoped>
+.question-title {
+  @apply text-28 text-fore-title break-words;
+}
+
+.question-options {
+  @apply mt-20;
+}
+
+.question-toolbar {
+  @apply flex items-center justify-between gap-20;
+}
+
+.question-parse {
+  @apply mt-30 flex flex-col gap-20;
+}
+
+.question-option {
+  @apply flex items-center px-30 py-24 bg-back rounded-8 border border-none border-transparent;
+
+  .question-option-index {
+    @apply w-40 h-40 rounded-full bg-transparent text-30 text-fore-light font-bold flex items-center justify-center flex-shrink-0;
+  }
+
+  .question-option-content {
+    @apply text-28 text-fore-title ml-20 flex-1 min-w-0;
+  }
+}
+
+.question-option-selected {
+  @apply bg-[#b5eaff8e];
+
+  .question-option-index {
+    @apply bg-primary text-white;
+  }
+
+  .question-option-content {
+    @apply text-primary;
+  }
+}
+
+.question-option-not-know {
+  @apply bg-[#b5eaff8e];
+
+  .question-option-content {
+    @apply text-primary;
+  }
+}
+
+.question-option-correct {
+  @apply bg-[#E7FCF8] border-[#E7FCF8] text-[#2CC6A0];
+
+  .question-option-index {
+    @apply text-[#2CC6A0];
+  }
+
+  .question-option-content {
+    @apply text-[#2CC6A0];
+  }
+}
+
+.question-option-miss {
+  @apply relative overflow-hidden;
+
+  &::before {
+    content: '';
+    position: absolute;
+    right: -56rpx;
+    top: 15rpx;
+    width: 180rpx;
+    height: 36rpx;
+    background: rgba(255, 91, 92, 0.2);
+    transform: rotate(30deg);
+    box-shadow: 0 2rpx 4rpx rgba(255, 91, 92, 0.1);
+  }
+
+  &::after {
+    content: '漏选';
+    position: absolute;
+    right: -8rpx;
+    top: 14rpx;
+    width: 100rpx;
+    height: 32rpx;
+    color: #FF5B5C;
+    font-size: 20rpx;
+    // font-weight: bold;
+    transform: rotate(30deg);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    line-height: 1;
+  }
+}
+
+.question-option-incorrect {
+  @apply bg-[#FEEDE9] border-[#FEEDE9] text-[#FF5B5C];
+
+  .question-option-index {
+    @apply text-[#FF5B5C];
+  }
+
+  .question-option-content {
+    @apply text-[#FF5B5C];
+  }
+
+}
+
+.question-option+.question-option {
+  @apply mt-24;
+}
+</style>

+ 1 - 1
src/pagesStudy/pages/index/compoentns/index-menu.vue

@@ -35,7 +35,7 @@ const menus = computed(() => [
   {
     label: '收藏夹',
     icon: '/menu/menu-favorite.png',
-    pageUrl: routes.pageCollect,
+    pageUrl: routes.pageQuestionFavorites,
     visible: true
   },
   {

+ 87 - 0
src/pagesStudy/pages/question-favorites/question-favorites.vue

@@ -0,0 +1,87 @@
+<template>
+  <ie-page :safe-area-inset-bottom="false">
+    <z-paging ref="paging" v-model="list" :safe-area-inset-bottom="true" :hide-no-more-by-limit="10"
+      @query="handleQuery">
+      <template #top>
+        <ie-navbar title="收藏夹" />
+      </template>
+      <view v-for="item in list" :key="item.id">
+        <question-book-item :data="item" @correct="handleCorrect" @cancelCollect="handleCancelCollect" />
+      </view>
+    </z-paging>
+    <question-correct-popup ref="questionCorrectPopupRef" />
+  </ie-page>
+</template>
+
+<script lang="ts" setup>
+import type { Study } from '@/types';
+import { getFavoriteQuestionList } from '@/api/modules/study';
+import QuestionBookItem from '@/pagesStudy/components/question-book-item.vue';
+import QuestionCorrectPopup from '@/pagesStudy/pages/exam-start/components/question-correct-popup.vue';
+import { decodeHtmlEntities } from '@/composables/useExam';
+import useQuestionBook from '@/composables/useQuestionBook';
+
+const list = ref<Study.FavoriteQuestionVO[]>([]);
+const paging = ref<ZPagingInstance>();
+const questionCorrectPopupRef = ref();
+const { cancelCollect, isOptionCorrect, isOptionSelected } = useQuestionBook();
+
+const transfomeToQuestionBookVO = (list: Study.FavoriteQuestion[]): Study.FavoriteQuestionVO[] => {
+  const orders = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
+  return list.map(item => {
+    const originalOptions = [item.optionA, item.optionB, item.optionC, item.optionD, item.optionE, item.optionF, item.optionG].filter(Boolean) as string[];
+    const options: Study.QuestionOption[] = originalOptions.map((option, index) => {
+      const cleanedOption = option.replace(/[A-Z]\./g, '').replace(/\s/g, ' ');
+      return {
+        name: decodeHtmlEntities(cleanedOption),
+        no: orders[index],
+        id: index,
+        isAnswer: false,
+        isCorrect: false,
+        isSelected: false,
+        isIncorrect: false,
+        isMissed: false
+      }
+    });
+    return {
+      id: item.id,
+      title: item.title,
+      options: options,
+      answers: [], // 收藏下没有答案
+      answer: decodeHtmlEntities(item.answer1 || ''),
+      qtype: item.qtpye,
+      typeId: item.typeId,
+      parse: decodeHtmlEntities(item.parse || ''),
+      collect: true
+    }
+  })
+}
+const handleQuery = (page: number, size: number) => {
+  getFavoriteQuestionList({ pageNum: page, pageSize: size, type: 'question', subjectId: 0 }).then(res => {
+    const data = transfomeToQuestionBookVO(res.rows);
+    // 补充状态
+    data.forEach(qs => {
+      qs.options.forEach(option => {
+        option.isCorrect = isOptionCorrect(qs, option);
+        option.isSelected = isOptionSelected(qs, option);
+        option.isMissed = !option.isSelected && option.isCorrect;
+        option.isIncorrect = !option.isCorrect && option.isSelected;
+      });
+    });
+    console.log(data)
+    paging.value?.completeByTotal(data, res.total);
+  });
+};
+const handleCorrect = (question: Study.FavoriteQuestionVO) => {
+  questionCorrectPopupRef.value.open(question.id);
+}
+const handleCancelCollect = (question: Study.FavoriteQuestionVO) => {
+  cancelCollect(question.id).then(res => {
+    if (res) {
+      paging.value?.refresh();
+    }
+  });
+}
+</script>
+
+<style lang="scss"></style>

+ 61 - 0
src/pagesStudy/pages/wrong-book/components/datetime-picker.vue

@@ -0,0 +1,61 @@
+<template>
+  <view class="bg-white py-20 px-30 text-26 ">
+    <view class="h-50 p-10 flex items-center gap-x-10  border border-[#999999] border-solid rounded-6"
+      @click="handlePicker">
+      <uv-icon name="calendar" size="20" color="#999999" />
+      <text class="flex-1 text-center" :class="[startDate ? 'text-fore-title' : 'text-[#999999]']">
+        {{ startDate || '开始日期' }}
+      </text>
+      <text class="text-[#999999]">至</text>
+      <text class="flex-1 text-center" :class="[endDate ? 'text-fore-title' : 'text-[#999999]']">
+        {{ endDate || '结束日期' }}
+      </text>
+      <view class="w-44">
+        <uv-icon name="close-circle" size="20" color="#999999" v-if="startDate || endDate" @click.stop="handleClear" />
+      </view>
+    </view>
+  </view>
+  <ie-popup ref="popupRef" title="选择日期" @confirm="handleConfirm">
+    <uni-calendar ref="calendarRef" :insert="true" :lunar="false" :range="true" :showMonth="false"
+      :showExtraInfo="false" start-date="" end-date="" @change="handleChange"></uni-calendar>
+  </ie-popup>
+</template>
+<script lang="ts" setup>
+const emit = defineEmits<{
+  (e: 'change', value: { startDate: string, endDate: string }): void
+}>();
+
+const startDate = defineModel<string>('startDate', { required: true });
+const endDate = defineModel<string>('endDate', { required: true });
+
+const currentStartDate = ref('');
+const currentEndDate = ref('');
+const handleChange = (e: any) => {
+  const { before, after } = e.range;
+  if (before && after) {
+    currentStartDate.value = before;
+    currentEndDate.value = after;
+  }
+}
+const popupRef = ref();
+const handlePicker = () => {
+  popupRef.value.open()
+}
+const handleClear = () => {
+  currentStartDate.value = ''
+  currentEndDate.value = ''
+  updateDate();
+}
+const updateDate = () => {
+  startDate.value = currentStartDate.value;
+  endDate.value = currentEndDate.value;
+  setTimeout(() => {
+    emit('change', { startDate: startDate.value, endDate: endDate.value });
+  }, 0);
+}
+const handleConfirm = () => {
+  updateDate();
+  popupRef.value.close();
+}
+</script>
+<style lang="scss" scoped></style>

+ 133 - 0
src/pagesStudy/pages/wrong-book/wrong-book.vue

@@ -0,0 +1,133 @@
+<template>
+  <ie-page :safe-area-inset-bottom="false">
+    <z-paging ref="paging" v-model="list" :auto="false" :safe-area-inset-bottom="true" :hide-no-more-by-limit="10"
+      @query="handleQuery">
+      <template #top>
+        <ie-navbar title="错题本" />
+        <uv-tabs :current="current" keyName="subjectName" :list="tabs" @change="handleTabChange" />
+        <uv-line margin="0" />
+        <date-time-picker v-model:start-date="startDate" v-model:end-date="endDate" @change="handleChange" />
+      </template>
+      <view v-for="item in list" :key="item.id">
+        <question-book-item :data="item" :show-answer="true" @correct="handleCorrect"
+          @toggleCollect="handleToggleCollect" @delete="handleDelete" />
+      </view>
+    </z-paging>
+    <question-correct-popup ref="questionCorrectPopupRef" />
+  </ie-page>
+</template>
+
+<script lang="ts" setup>
+import type { Study } from '@/types';
+import QuestionBookItem from '@/pagesStudy/components/question-book-item.vue';
+import QuestionCorrectPopup from '@/pagesStudy/pages/exam-start/components/question-correct-popup.vue';
+import DateTimePicker from '@/pagesStudy/pages/wrong-book/components/datetime-picker.vue';
+import { decodeHtmlEntities } from '@/composables/useExam';
+import { getStudentSubject, getWrongBookList } from '@/api/modules/study';
+import useQuestionBook from '@/composables/useQuestionBook';
+
+const list = ref<Study.FavoriteQuestionVO[]>([]);
+const paging = ref<ZPagingInstance>();
+const questionCorrectPopupRef = ref();
+const current = ref(0);
+const tabs = ref<Study.StudentSubject[]>([]);
+const startDate = ref('');
+const endDate = ref('');
+const { isOptionCorrect, isOptionSelected, cancelCollect, collect, deleteWrongQuestion } = useQuestionBook();
+
+const transfomeToQuestionBookVO = (list: Study.WrongBookQuestion[]): Study.FavoriteQuestionVO[] => {
+  const orders = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
+  return list.map(item => {
+    const originalOptions = [...item.options].filter(Boolean) as string[];
+    const options: Study.QuestionOption[] = originalOptions.map((option, index) => {
+      const cleanedOption = option.replace(/[A-Z]\./g, '').replace(/\s/g, ' ');
+      return {
+        name: decodeHtmlEntities(cleanedOption),
+        no: orders[index],
+        id: index,
+        isAnswer: false,
+        isCorrect: false,
+        isSelected: false,
+        isIncorrect: false,
+        isMissed: false
+      }
+    });
+    return {
+      id: item.questionId,
+      title: item.title,
+      options: options,
+      answers: item.answers,
+      answer: decodeHtmlEntities(item.answer1 || ''),
+      qtype: item.type,
+      typeId: item.typeId,
+      parse: decodeHtmlEntities(item.parse || ''),
+      knowledge: decodeHtmlEntities(item.knowledge || ''),
+      collect: item.collect || false
+    }
+  })
+}
+const handleQuery = (page: number, size: number) => {
+  const queryParams: Study.WrongBookQuestionRequestDTO = {
+    pageNum: page,
+    pageSize: size,
+    subjectId: tabs.value[current.value].subjectId,
+    start: startDate.value,
+    end: endDate.value
+  }
+  getWrongBookList(queryParams).then(res => {
+    const data = transfomeToQuestionBookVO(res.rows);
+    data.forEach(qs => {
+      qs.options.forEach(option => {
+        option.isCorrect = isOptionCorrect(qs, option);
+        option.isSelected = isOptionSelected(qs, option);
+        option.isMissed = !option.isSelected && option.isCorrect;
+        option.isIncorrect = !option.isCorrect && option.isSelected;
+      });
+    });
+    paging.value?.completeByTotal(data, res.total);
+  });
+};
+const handleCorrect = (question: Study.FavoriteQuestionVO) => {
+  questionCorrectPopupRef.value.open(question.id);
+}
+const handleToggleCollect = (question: Study.FavoriteQuestionVO) => {
+  if (question.collect) {
+    cancelCollect(question.id).then(res => {
+      if (res) {
+        paging.value?.refresh();
+      }
+    });
+  } else {
+    collect(question.id).then(res => {
+      if (res) {
+        paging.value?.refresh();
+      }
+    });
+  }
+}
+const handleDelete = (question: Study.FavoriteQuestionVO) => {
+  deleteWrongQuestion(question.id).then(res => {
+    if (res) {
+      paging.value?.reload();
+    }
+  });
+}
+const handleTabChange = (e: any) => {
+  current.value = e.index;
+  paging.value?.reload();
+}
+const handleChange = (e: any) => {
+  paging.value?.reload();
+}
+const loadData = () => {
+  getStudentSubject().then(res => {
+    tabs.value = res.data;
+    paging.value?.reload();
+  });
+}
+onLoad(() => {
+  loadData();
+});
+</script>
+
+<style lang="scss"></style>

+ 13 - 0
src/pagesSystem/pages/startup/startup.vue

@@ -0,0 +1,13 @@
+<template>
+</template>
+
+<script lang="ts" setup>
+import { useTransferPage } from '@/hooks/useTransferPage';
+const { prevData, transferTo, routes } = useTransferPage();
+
+onLoad(() => {
+  console.log(prevData)
+});
+</script>
+
+<style lang="scss"></style>

+ 154 - 2
src/types/study.ts

@@ -19,7 +19,7 @@ export interface StudentStat {
 /**
  * 班级知识点记录
  */
-  export interface ClassKnowledgeRecord {
+export interface ClassKnowledgeRecord {
   rate: number;
   list: StudentPlanStudyRecord[];
 }
@@ -455,4 +455,156 @@ export interface PaperWork {
   endTime: string;
   duration: number;
   batchName: string;
-}
+}
+
+
+/**
+ * 收藏题目列表请求参数
+ */
+export interface FavoriteQuestionListRequestDTO {
+  pageNum: number;
+  pageSize: number;
+  type: string;
+  subjectId: number;
+}
+/**
+ * 收藏题目
+ */
+export interface FavoriteQuestion {
+  answer0: string | null;
+  answer1: string | null;
+  answer2: string | null;
+  area: string | null;
+  collect: boolean;
+  createBy: string | null;
+  createTime: string;
+  diff: number;
+  fromSite: string;
+  gradeId: number;
+  id: number;
+  isKonw: boolean | null;
+  isNormal: boolean | null;
+  isSub: boolean | null;
+  isSubType: string;
+  isUpdate: number;
+  isunique: boolean | null;
+  knowId: number | null;
+  knowledgeId: number;
+  knowledges: any | null;
+  md5: string | null;
+  md52: string | null;
+  number: string | null;
+  optionA: string;
+  optionB: string;
+  optionC: string;
+  optionD: string;
+  optionE: string | null;
+  optionF: string | null;
+  optionG: string | null;
+  options: any | null;
+  options0: any | null;
+  paperId: number | null;
+  paperTpye: string | null;
+  paperTypeTitle: string | null;
+  parse: string;
+  parse0: string | null;
+  qtpye: string;
+  remark: string | null;
+  score: number;
+  similarity: number;
+  source: string;
+  subCnt: number | null;
+  subjectId: number;
+  tiid: string;
+  title: string;
+  title0: string;
+  title1: string | null;
+  typeId: number;
+  updateBy: string | null;
+  updateTime: string | null;
+  userId: number | null;
+  year: number;
+}
+
+/**
+ * 收藏题目VO
+ */
+export interface FavoriteQuestionVO {
+  id: number;
+  title: string;
+  options: QuestionOption[];
+  answers: string[];
+  answer: string;
+  parse: string;
+  qtype: string;
+  typeId: number;
+  knowledge?: string;
+  collect: boolean;
+}
+
+/**
+ * 学生科目
+ */
+export interface StudentSubject {
+  createBy: string | null;
+  createTime: string | null;
+  updateBy: string | null;
+  updateTime: string | null;
+  remark: string | null;
+  subjectId: number;
+  subjectName: string;
+  pinyin: string | null;
+  sort: number;
+  locations: string;
+  examTypes: string;
+  groupName: string;
+}
+
+
+export interface WrongBookQuestionRequestDTO {
+  pageNum: number;
+  pageSize: number;
+  subjectId: number;
+  start?: string;
+  end?: string;
+}
+
+/**
+ * 错题本题目
+ */
+export interface WrongBookQuestion {
+  createBy: string | null;
+  createTime: string | null;
+  updateBy: string | null;
+  updateTime: string | null;
+  remark: string | null;
+  wrongId: number;
+  studentId: number | null;
+  questionId: number;
+  source: string | null;
+  state: number;
+  knownledgeId: number;
+  subjectId: number | null;
+  paperId: number | null;
+  answer: string;
+  answer1: string;
+  answer2: string;
+  scoreTotal: number | null;
+  score: number | null;
+  scoreLevel: string | null;
+  scoreRate: number | null;
+  wrongCount: number;
+  rightCount: number | null;
+  totalCount: number | null;
+  createdTime: string | null;
+  updatedTime: string | null;
+  collect: boolean | null;
+  knownledgeName: string | null;
+  title: string;
+  knowledge: string;
+  parse: string;
+  typeId: number;
+  type: string;
+  options: string[];
+  answers: string[];
+}