Bläddra i källkod

feat: 导出主题流程

zhangjie 1 år sedan
förälder
incheckning
38cdf9a9c8

+ 9 - 5
electron/db/enumerate.ts

@@ -1,15 +1,19 @@
 // 任务状态:
-// INIT:未开始,RUNNING:运行中,FINISH:已完成
+// INIT:未开始,READY:数据完成,FINISH:已完成
 export const TRACK_TASK_STATUS = {
   INIT: 0,
-  RUNNING: 1,
+  READY: 1,
   FINISH: 9,
 };
+
+export type TrackTaskStatusKey = keyof typeof TRACK_TASK_STATUS;
+
 // 细分任务状态:
-// INIT:未开始,READY:数据已完整,RUNNING:运行中,FINISH:已完成
+// INIT:未开始,RUNNING:运行中,FINISH:已完成
 export const TRACK_TASK_DETAIL_STATUS = {
   INIT: 0,
-  READY: 1,
-  RUNNING: 2,
+  RUNNING: 1,
   FINISH: 9,
 };
+
+export type TrackTaskDetailStatusKey = keyof typeof TRACK_TASK_DETAIL_STATUS;

+ 53 - 5
electron/db/modelApi/trackTask.ts

@@ -1,14 +1,19 @@
 import { Op } from 'sequelize';
 
 import TrackTask, { TrackTaskCreationAttributes } from '../models/trackTask';
-import TrackTeskDetail, {
+import TrackTaskDetail, {
   TrackTaskDetailCreationAttributes,
 } from '../models/trackTaskDetail';
 import sequelize from '../sequelizeInstance';
+import {
+  TrackTaskStatusKey,
+  TrackTaskDetailStatusKey,
+  TRACK_TASK_STATUS,
+} from '../enumerate';
 
 export async function getUnfinishTrackTask() {
   const task = await TrackTask.findOne({
-    where: { status: 0 },
+    where: { status: { [Op.ne]: TRACK_TASK_STATUS.FINISH } },
   }).catch((err) => {
     console.dir(err);
   });
@@ -24,11 +29,12 @@ export async function createTrackTask(
   try {
     await TrackTask.update(
       {
-        status: 2,
+        status: TRACK_TASK_STATUS.FINISH,
+        error: '补漏结束',
       },
       {
         where: {
-          status: { [Op.or]: [0, 1] },
+          status: { [Op.ne]: TRACK_TASK_STATUS.FINISH },
         },
         transaction: t,
       }
@@ -40,7 +46,7 @@ export async function createTrackTask(
     details.forEach((item) => {
       item.trackTaskId = task.id;
     });
-    await TrackTeskDetail.bulkCreate(details, { transaction: t });
+    await TrackTaskDetail.bulkCreate(details, { transaction: t });
 
     await t.commit();
 
@@ -50,3 +56,45 @@ export async function createTrackTask(
     return Promise.reject(error);
   }
 }
+
+export async function updateTrackTaskStatus(data: {
+  id: number;
+  status: TrackTaskStatusKey;
+}) {
+  await TrackTask.update(
+    {
+      status: TRACK_TASK_STATUS[data.status],
+    },
+    {
+      where: {
+        id: data.id,
+      },
+    }
+  );
+}
+
+export async function createTrackTaskDetails(
+  details: TrackTaskDetailCreationAttributes[]
+) {
+  const t = await sequelize.transaction();
+  try {
+    await TrackTaskDetail.bulkCreate(details, { transaction: t });
+    await t.commit();
+
+    return true;
+  } catch (error) {
+    await t.rollback();
+    return Promise.reject(error);
+  }
+}
+
+// track detail ------------------->
+export async function getTrackTaskDetailCount(data: {
+  trackTaskId: number;
+  status?: TrackTaskDetailStatusKey;
+}) {
+  const count = await TrackTaskDetail.count({
+    where: data,
+  });
+  return count;
+}

+ 42 - 4
electron/db/models/trackTask.ts

@@ -20,9 +20,19 @@ class TrackTask extends Model<
 
   declare semesterId: string;
 
+  declare semesterName: string;
+
   declare examId: string;
 
-  declare extType: string;
+  declare examName: string;
+
+  declare courseId: string | null;
+
+  declare courseName: string | null;
+
+  declare courseCode: string | null;
+
+  declare paperNumber: string | null;
 
   declare pictureType: string;
 
@@ -30,6 +40,8 @@ class TrackTask extends Model<
 
   declare status: number;
 
+  declare error: string | null;
+
   declare createdAt: CreationOptional<Date>;
 
   declare updatedAt: CreationOptional<Date>;
@@ -50,14 +62,33 @@ TrackTask.init(
       type: DataTypes.STRING,
       allowNull: false,
     },
+    semesterName: {
+      type: DataTypes.STRING,
+      allowNull: false,
+    },
     examId: {
       type: DataTypes.STRING,
       allowNull: false,
     },
-    extType: {
+    examName: {
       type: DataTypes.STRING,
       allowNull: false,
-      comment: '文件类型',
+    },
+    courseId: {
+      type: DataTypes.STRING,
+      allowNull: true,
+    },
+    courseName: {
+      type: DataTypes.STRING,
+      allowNull: true,
+    },
+    courseCode: {
+      type: DataTypes.STRING,
+      allowNull: true,
+    },
+    paperNumber: {
+      type: DataTypes.STRING,
+      allowNull: true,
     },
     pictureType: {
       type: DataTypes.STRING,
@@ -74,6 +105,11 @@ TrackTask.init(
       allowNull: false,
       defaultValue: TRACK_TASK_STATUS.INIT,
     },
+    error: {
+      type: DataTypes.STRING,
+      allowNull: true,
+      comment: '错误信息',
+    },
     createdAt: DataTypes.DATE,
     updatedAt: DataTypes.DATE,
   },
@@ -87,7 +123,9 @@ TrackTask.init(
 
 export type TrackTaskCreationAttributes = InferCreationAttributes<
   TrackTask,
-  { omit: 'id' | 'createdAt' | 'updatedAt' }
+  { omit: 'id' | 'createdAt' | 'updatedAt' | 'error' }
 >;
 
+export type TrackTaskData = InferAttributes<TrackTask>;
+
 export default TrackTask;

+ 7 - 13
electron/db/models/trackTaskDetail.ts

@@ -19,14 +19,12 @@ class TrackTaskDetail extends Model<
 
   declare trackTaskId: number;
 
+  declare studentId: string;
+
   declare studentName: string;
 
   declare studentCode: string;
 
-  declare courseName: string;
-
-  declare courseCode: string;
-
   declare status: number;
 
   declare error: string | null;
@@ -47,19 +45,15 @@ TrackTaskDetail.init(
       type: DataTypes.INTEGER,
       allowNull: false,
     },
-    studentName: {
+    studentId: {
       type: DataTypes.STRING,
       allowNull: false,
     },
-    studentCode: {
-      type: DataTypes.STRING,
-      allowNull: false,
-    },
-    courseName: {
+    studentName: {
       type: DataTypes.STRING,
       allowNull: false,
     },
-    courseCode: {
+    studentCode: {
       type: DataTypes.STRING,
       allowNull: false,
     },
@@ -69,7 +63,7 @@ TrackTaskDetail.init(
       defaultValue: TRACK_TASK_DETAIL_STATUS.INIT,
     },
     error: {
-      type: DataTypes.TEXT,
+      type: DataTypes.STRING,
       allowNull: true,
       comment: '错误信息',
     },
@@ -89,7 +83,7 @@ TrackTaskDetail.belongsTo(TrackTask);
 
 export type TrackTaskDetailCreationAttributes = InferCreationAttributes<
   TrackTaskDetail,
-  { omit: 'id' | 'createdAt' | 'updatedAt' }
+  { omit: 'id' | 'createdAt' | 'updatedAt' | 'error' }
 >;
 
 export default TrackTaskDetail;

+ 3 - 2
electron/main/useWinProcess.ts

@@ -39,13 +39,14 @@ async function buildChildWindow(
   });
   // const exportPdfHash = '#/export-track-pdf';
 
+  const pageUrl = `${loadPageUrl}&winId=${childWin.id}`;
   if (process.env.WEBPACK_DEV_SERVER_URL) {
     // Load the url of the dev server if in development mode
-    await childWin.loadURL(process.env.WEBPACK_DEV_SERVER_URL + loadPageUrl);
+    await childWin.loadURL(process.env.WEBPACK_DEV_SERVER_URL + pageUrl);
     // childWin.webContents.openDevTools();
   } else {
     // Load the index.html when not in development
-    await childWin.loadURL(`app://./index.html${loadPageUrl}`);
+    await childWin.loadURL(`app://./index.html${pageUrl}`);
   }
   return childWin;
 }

+ 10 - 4
electron/preload/apiDb.ts

@@ -1,17 +1,23 @@
+import createDb from '../db/createdb';
+import { getDict, updateDict } from '../db/modelApi/dict';
 import {
   getUnfinishTrackTask,
   createTrackTask,
+  createTrackTaskDetails,
+  updateTrackTaskStatus,
+  getTrackTaskDetailCount,
 } from '../db/modelApi/trackTask';
-import { getDict, updateDict } from '../db/modelApi/dict';
-import createDb from '../db/createdb';
 
 createDb();
 
 const dbApi = {
-  getUnfinishTrackTask,
-  createTrackTask,
   getDict,
   updateDict,
+  getUnfinishTrackTask,
+  createTrackTask,
+  createTrackTaskDetails,
+  updateTrackTaskStatus,
+  getTrackTaskDetailCount,
 };
 
 export type DbApi = typeof dbApi;

+ 9 - 1
src/api/task.ts

@@ -8,8 +8,10 @@ import {
   CourseItem,
   TrackExportListParams,
   TrackExportListPageRes,
+  TrackExportDetailListParams,
   CourseQueryParams,
   PaperNumberQueryParams,
+  TrackExportDetailListPageRes,
 } from './types/task';
 
 // semester
@@ -49,12 +51,18 @@ export function paperNumberQuery(
   );
 }
 
-// 成绩查询列表
+// 导出科目查询列表
 export async function trackExportListPage(
   params: TrackExportListParams
 ): Promise<TrackExportListPageRes> {
   return axios.post('/api/admin/mark/setting/scoreList', {}, { params });
 }
+// 导出科目学生明细查询列表
+export async function trackExportDetailListPage(
+  params: TrackExportDetailListParams
+): Promise<TrackExportDetailListPageRes> {
+  return axios.post('/api/admin/mark/student/score', {}, { params });
+}
 
 /** 查看单个学生的试卷轨迹 */
 export async function getSingleStudentTaskOfStudentTrack(

+ 19 - 0
src/api/types/task.ts

@@ -191,3 +191,22 @@ export interface TrackExportItem {
   uploadCount: number;
 }
 export type TrackExportListPageRes = PageResult<TrackExportItem>;
+
+export interface TrackExportDetailListFilter {
+  examId: string;
+  paperNumber: string;
+  filter: number;
+}
+
+export type TrackExportDetailListParams =
+  PageParams<TrackExportDetailListFilter>;
+
+export interface TrackExportDetailItem {
+  studentId: string;
+  courseCode: string;
+  courseName: string;
+  paperNumber: string;
+  studentName: string;
+  studentCode: string;
+}
+export type TrackExportDetailListPageRes = PageResult<TrackExportDetailItem>;

+ 31 - 0
src/components/fullscreen-loading/index.vue

@@ -0,0 +1,31 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    :width="500"
+    :mask-closable="false"
+    :esc-to-close="false"
+    :closable="false"
+    :footer="false"
+    hide-title
+  >
+    <div class="part-box">
+      <a-spin :tip="tips" />
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+  import useModal from '@/hooks/modal';
+
+  defineOptions({
+    name: 'FullscreenLoading',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  defineProps<{
+    tips: string;
+  }>();
+</script>

+ 21 - 0
src/hooks/timout.ts

@@ -0,0 +1,21 @@
+export default function useTimeout() {
+  const times: Record<string, NodeJS.Timeout[]> = {};
+
+  function addSetTimeout(key: string, action: () => void, time: number) {
+    if (!times[key]) times[key] = [] as NodeJS.Timeout[];
+    times[key].push(setTimeout(action, time));
+  }
+
+  function clearSetTimeout(key: string) {
+    if (!times[key]) return;
+    times[key].forEach((item) => {
+      clearTimeout(item);
+    });
+    times[key] = [];
+  }
+
+  return {
+    addSetTimeout,
+    clearSetTimeout,
+  };
+}

+ 1 - 0
src/store/modules/app/index.ts

@@ -9,6 +9,7 @@ const useAppStore = defineStore('app', {
     trackConfig: {
       pictureType: ['track'],
       outputDir: '',
+      curOutputDir: '',
       outputDirIsDefault: true,
     },
   }),

+ 1 - 0
src/store/modules/app/types.ts

@@ -3,6 +3,7 @@ import { PictureTypeEnum } from '@/constants/enumerate';
 export interface TrackConfigType {
   pictureType: PictureTypeEnum[];
   outputDir: string;
+  curOutputDir: string;
   outputDirIsDefault: boolean;
 }
 

+ 171 - 8
src/views/base/track-export/index.vue

@@ -6,6 +6,7 @@
         :clearable="false"
         select-default
         placeholder="请选择"
+        @change="semesterChange"
       />
       <SelectExam
         v-model="searchModel.examId"
@@ -14,12 +15,14 @@
         select-default
         placeholder="请选择"
         @default-selected="toPage(1)"
+        @change="examChange"
       />
       <SelectCourse
         v-model="searchModel.courseId"
         :semester-id="searchModel.semesterId"
         :exam-id="searchModel.examId"
         placeholder="请选择"
+        @change="courseChange"
       />
       <SelectPaperNumber
         v-model="searchModel.paperNumber"
@@ -71,6 +74,14 @@
 
   <!-- ModifySet -->
   <ModifySet ref="modifySetRef" />
+  <TaskProgress ref="taskProgressRef" />
+
+  <!-- data loading tips -->
+  <TaskDetailBuildProgess
+    ref="taskDetailBuildProgessRef"
+    :task-id="trackTaskId"
+    :task-stop="detailBuildStop"
+  />
 </template>
 
 <script setup lang="ts">
@@ -78,19 +89,26 @@
   import { Message, TableColumnData } from '@arco-design/web-vue';
 
   import useTable from '@/hooks/table';
+  import useLoading from '@/hooks/loading';
+
   import { courseNameCodeFilter } from '@/utils/filter';
-  import { TrackExportItem } from '@/api/types/task';
+  import { CourseItem, TrackExportItem } from '@/api/types/task';
   import { trackExportListPage } from '@/api/task';
   import { TrackConfigType } from '@/store/modules/app/types';
-  import { useAppStore } from '@/store';
+  import { useAppStore, useUserStore } from '@/store';
+  import { OptionListItem } from '@/types/global';
+  import useTask from './useTask';
 
+  import TaskDetailBuildProgess from './taskDetailBuildProgess.vue';
   import ModifySet from './modifySet.vue';
+  import TaskProgress from './taskProgress.vue';
 
   defineOptions({
     name: 'TrackExport',
   });
 
   const appStore = useAppStore();
+  const userStore = useUserStore();
 
   const searchModel = reactive({
     semesterId: '',
@@ -126,29 +144,174 @@
     false
   );
 
+  const seNames = {
+    semesterName: '',
+    examName: '',
+    courseName: '',
+    courseCode: '',
+  };
+
+  function semesterChange(val: OptionListItem) {
+    seNames.semesterName = val.label;
+    seNames.examName = '';
+    seNames.courseName = '';
+    seNames.courseCode = '';
+  }
+  function examChange(val: OptionListItem) {
+    seNames.examName = val.label;
+    seNames.courseName = '';
+    seNames.courseCode = '';
+  }
+  function courseChange(val: CourseItem) {
+    seNames.courseName = val.name;
+    seNames.courseCode = val.code;
+  }
+
   // table action
   const modifySetRef = ref();
   function toSet() {
     modifySetRef.value?.open();
   }
+
+  const {
+    trackTaskId,
+    createTrackTask,
+    updateTrackTaskReady,
+    getTrackExportDetailList,
+    getTrackExportList,
+  } = useTask();
+  const taskProgressRef = ref();
+  const taskDetailBuildProgessRef = ref();
+  const detailBuildStop = ref(false);
+
   function checkTrackConfigExist() {
     return Boolean(
       appStore.trackConfig.outputDir && appStore.trackConfig.pictureType.length
     );
   }
-  function toBatchDownload() {
+
+  // 下载前的检查
+  async function downloadCheck() {
     if (!checkTrackConfigExist()) {
       Message.error('请先编辑下载设置');
+      return false;
+    }
+
+    if (!appStore.trackConfig.outputDirIsDefault) {
+      const result = await window.electron.dialogSelectFile({
+        title: '选择保存目录',
+        properties: ['openDirectory'],
+      });
+
+      if (result.canceled) return false;
+
+      appStore.setInfo({
+        trackConfig: {
+          ...appStore.trackConfig,
+          curOutputDir: result.filePaths[0],
+        },
+      });
+    }
+
+    return true;
+  }
+  const { loading, setLoading } = useLoading();
+  // 批量下载
+  async function toBatchDownload() {
+    detailBuildStop.value = false;
+    if (loading.value) return;
+
+    const res = await downloadCheck();
+    if (!res) return;
+
+    setLoading(true);
+    let result = true;
+    await createTrackTask({
+      ...searchModel,
+      ...seNames,
+      schoolId: userStore.curSchoolInfo.id,
+      pictureType: appStore.trackConfig.pictureType.join(),
+      outputDir: appStore.trackConfig.curOutputDir,
+      status: 0,
+    }).catch(() => {
+      result = false;
+      setLoading(false);
+    });
+    if (!result) {
+      Message.error('创建任务错误!');
       return;
     }
-    console.log('batch');
+
+    // 构建任务提示
+    taskDetailBuildProgessRef.value?.open();
+
+    // 开始构建任务
+    let tRes = true;
+    await getTrackExportList(searchModel).catch((error) => {
+      console.log(error);
+      tRes = false;
+    });
+    setLoading(false);
+
+    if (!tRes) {
+      detailBuildStop.value = true;
+      Message.error('创建任务详情错误!');
+      return;
+    }
+
+    await updateTrackTaskReady();
+    detailBuildStop.value = true;
+    taskProgressRef.value?.open();
   }
-  function toDownload(row: TrackExportItem) {
-    if (!checkTrackConfigExist()) {
-      Message.error('请先编辑下载设置');
+
+  // 单个课程下载
+  async function toDownload(row: TrackExportItem) {
+    detailBuildStop.value = false;
+    if (loading.value) return;
+
+    const res = await downloadCheck();
+    if (!res) return;
+
+    setLoading(true);
+    let result = true;
+    await createTrackTask({
+      ...searchModel,
+      ...seNames,
+      courseCode: row.courseCode,
+      courseName: row.courseName,
+      paperNumber: row.paperNumber,
+      schoolId: userStore.curSchoolInfo.id,
+      pictureType: appStore.trackConfig.pictureType.join(),
+      outputDir: appStore.trackConfig.curOutputDir,
+      status: 0,
+    }).catch(() => {
+      result = false;
+      setLoading(false);
+    });
+    if (!result) {
+      Message.error('创建任务错误!');
       return;
     }
-    console.log('download', row);
+
+    // 构建任务提示
+    taskDetailBuildProgessRef.value?.open();
+
+    // 开始构建任务
+    let tRes = true;
+    await getTrackExportDetailList(row).catch((error) => {
+      console.log(error);
+      tRes = false;
+    });
+    setLoading(false);
+
+    if (!tRes) {
+      Message.error('创建任务详情错误!');
+      return;
+    }
+
+    await updateTrackTaskReady();
+    detailBuildStop.value = true;
+    taskProgressRef.value?.open();
   }
 
   async function initData() {

+ 3 - 0
src/views/base/track-export/modifySet.vue

@@ -80,6 +80,7 @@
   const defaultFormData = {
     pictureType: ['track'] as PictureTypeEnum[],
     outputDir: '',
+    curOutputDir: '',
     outputDirIsDefault: true,
   };
 
@@ -121,6 +122,8 @@
 
     setLoading(true);
     const datas = objAssign(formData, {});
+    if (datas.outputDirIsDefault) datas.curOutputDir = datas.outputDir;
+
     let res = true;
     await window.db
       .updateDict({ key: 'trackConfig', val: JSON.stringify(datas) })

+ 58 - 0
src/views/base/track-export/taskDetailBuildProgess.vue

@@ -0,0 +1,58 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    :width="500"
+    :mask-closable="false"
+    :esc-to-close="false"
+    :closable="false"
+    :footer="false"
+    hide-title
+    @before-open="modalBeforeOpen"
+  >
+    <div class="part-box">
+      <a-spin :tip="taskTips" />
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import useTimeout from '@/hooks/timout';
+  import useModal from '@/hooks/modal';
+
+  defineOptions({
+    name: 'TaskDetailBuildProgess',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  const props = defineProps<{
+    taskId: number;
+    taskStop: boolean;
+  }>();
+
+  const taskTips = ref('');
+
+  const { addSetTimeout, clearSetTimeout } = useTimeout();
+
+  async function getTaskDetailCount() {
+    clearSetTimeout('count');
+    const trackTaskDetailCount = await window.db.getTrackTaskDetailCount({
+      trackTaskId: props.taskId,
+    });
+    taskTips.value = `正在构建下载任务,数量${trackTaskDetailCount}`;
+
+    if (props.taskStop) {
+      taskTips.value = '';
+      close();
+      return;
+    }
+    addSetTimeout('count', getTaskDetailCount, 1 * 1000);
+  }
+
+  function modalBeforeOpen() {
+    getTaskDetailCount();
+  }
+</script>

+ 134 - 0
src/views/base/track-export/taskProgress.vue

@@ -0,0 +1,134 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    :width="500"
+    title-align="start"
+    top="10vh"
+    :align-center="false"
+    :mask-closable="false"
+    :esc-to-close="false"
+    @before-open="modalBeforeOpen"
+  >
+    <template #title> 任务进度 </template>
+
+    <a-descriptions
+      :data="taskInfo"
+      title="任务详情"
+      layout="inline-horizontal"
+      :align="{ label: 'right' }"
+    />
+
+    <a-descriptions title="进度">
+      <a-descriptions-item label="">
+        <a-progress :percent="progressNum" :stroke-width="10" />
+      </a-descriptions-item>
+    </a-descriptions>
+
+    <template #footer>
+      <a-button @click="close">取消</a-button>
+    </template>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { DescData, Message } from '@arco-design/web-vue';
+  import useModal from '@/hooks/modal';
+  import useTimeout from '@/hooks/timout';
+  import { PICTURE_TYPE, PictureTypeEnum } from '@/constants/enumerate';
+  import { TrackTaskData } from 'electron/db/models/trackTask';
+  import { useUserStore } from '@/store';
+
+  defineOptions({
+    name: 'ModifySet',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  const { addSetTimeout, clearSetTimeout } = useTimeout();
+  const userStore = useUserStore();
+
+  const PROGRESS_KEY = 'progress';
+
+  const taskInfo = ref<DescData[]>([]);
+  const task = ref<TrackTaskData>();
+  const total = ref(0);
+  const finishCount = ref(0);
+  const progressNum = ref(0);
+
+  async function updateProgress() {
+    clearSetTimeout(PROGRESS_KEY);
+    finishCount.value = await window.db.getTrackTaskDetailCount({
+      trackTaskId: task.value?.id as number,
+      status: 'FINISH',
+    });
+    progressNum.value = !total.value
+      ? 0
+      : Math.floor((10000 * finishCount.value) / total.value) / 10000;
+
+    addSetTimeout(PROGRESS_KEY, updateProgress, 1 * 1000);
+  }
+
+  function getExportUrl() {
+    const page = '#/track-task-export';
+    const user = window.btoa(JSON.stringify(userStore.userInfo));
+    return `${page}?user=${user}`;
+  }
+
+  /* init modal */
+  async function modalBeforeOpen() {
+    const res = await window.db.getUnfinishTrackTask();
+    if (!res) {
+      Message.warning('没有未完成任务');
+      close();
+      return;
+    }
+
+    task.value = res;
+    taskInfo.value = [
+      {
+        value: res.semesterName,
+        label: '学期',
+      },
+      {
+        value: res.examName,
+        label: '考试',
+      },
+    ];
+    if (res.courseId) {
+      taskInfo.value.push({
+        value: `${res.courseName}(${res.courseCode})`,
+        label: '科目',
+      });
+    }
+    if (res.paperNumber) {
+      taskInfo.value.push({
+        value: res.paperNumber,
+        label: '试卷编码',
+      });
+    }
+    taskInfo.value.push(
+      ...[
+        {
+          value: (res.pictureType.split(',') as PictureTypeEnum[])
+            .map((k) => PICTURE_TYPE[k])
+            .join(','),
+          label: '下载文件',
+        },
+        {
+          value: res.outputDir,
+          label: '保存目录',
+        },
+      ]
+    );
+
+    total.value = await window.db.getTrackTaskDetailCount({
+      trackTaskId: res.id,
+    });
+    updateProgress();
+
+    window.electron.startWinProcess(2, getExportUrl());
+  }
+</script>

+ 1 - 1
src/views/base/track-export/useTrack.ts → src/views/base/track-export/useDraw.ts

@@ -68,7 +68,7 @@ interface PaperRecogData {
   }>;
 }
 
-export default function useTrack() {
+export default function useDraw() {
   let answerMap = {} as AnswerMap;
   let cardData = [] as CardDataItem[];
   let recogDatas: string[] = [];

+ 121 - 0
src/views/base/track-export/useTask.ts

@@ -0,0 +1,121 @@
+import { ref } from 'vue';
+import { trackExportDetailListPage, trackExportListPage } from '@/api/task';
+import {
+  TrackExportDetailListParams,
+  TrackExportItem,
+  TrackExportListFilter,
+  TrackExportListPageRes,
+} from '@/api/types/task';
+import {
+  TRACK_TASK_DETAIL_STATUS,
+  TRACK_TASK_STATUS,
+} from 'electron/db/enumerate';
+import { TrackTaskCreationAttributes } from 'electron/db/models/trackTask';
+
+export default function useTask() {
+  const pageSize = 20;
+  const trackTaskId = ref(0);
+
+  async function createTrackTask(data: TrackTaskCreationAttributes) {
+    const res = await window.db.createTrackTask(
+      { ...data, status: TRACK_TASK_STATUS.INIT },
+      []
+    );
+    trackTaskId.value = res.id;
+  }
+  async function updateTrackTaskReady() {
+    await window.db.updateTrackTaskStatus({
+      id: trackTaskId.value,
+      status: 'FINISH',
+    });
+  }
+
+  async function getTrackExportList(params: TrackExportListFilter) {
+    const trackExportList: TrackExportItem[] = [];
+    const res = await trackExportListPage({
+      ...params,
+      pageNumber: 1,
+      pageSize,
+    });
+    trackExportList.push(...res.records);
+
+    if (res.pages > 1) {
+      const fetchFunc: Promise<TrackExportListPageRes>[] = [];
+      for (let i = 2; i <= res.pages; i++) {
+        fetchFunc.push(
+          trackExportListPage({
+            ...params,
+            pageNumber: i,
+            pageSize,
+          })
+        );
+      }
+
+      const fetchResult = await Promise.all(fetchFunc);
+      fetchResult.forEach((item) => {
+        trackExportList.push(...item.records);
+      });
+    }
+
+    // get details
+    const detailFetchFunc = trackExportList.map((item) =>
+      getTrackExportDetailList(item)
+    );
+    await Promise.all(detailFetchFunc);
+  }
+
+  async function getTrackExportDetailList(data: TrackExportItem) {
+    const filterData = {
+      filter: 0,
+      examId: data.examId,
+      paperNumber: data.paperNumber,
+      pageSize,
+    };
+    const res = await createTrackTaskDetails({
+      ...filterData,
+      pageNumber: 1,
+    });
+
+    if (res.pages <= 1) return;
+
+    const fetchFunc: Promise<any>[] = [];
+    for (let i = 2; i <= res.pages; i++) {
+      fetchFunc.push(
+        createTrackTaskDetails({
+          ...filterData,
+          pageNumber: i,
+        })
+      );
+    }
+
+    await Promise.all(fetchFunc);
+  }
+
+  async function createTrackTaskDetails(params: TrackExportDetailListParams) {
+    const res = await trackExportDetailListPage(params);
+    const details = res.records.map((item) => {
+      return {
+        trackTaskId: trackTaskId.value,
+        studentId: item.studentId,
+        studentName: item.studentName,
+        studentCode: item.studentCode,
+        status: TRACK_TASK_DETAIL_STATUS.INIT,
+      };
+    });
+    await window.db.createTrackTaskDetails(details);
+
+    return {
+      total: res.total,
+      pages: res.pages,
+      pageNumber: params.pageNumber,
+    };
+  }
+
+  return {
+    trackTaskId,
+    createTrackTask,
+    updateTrackTaskReady,
+    getTrackExportList,
+    getTrackExportDetailList,
+  };
+}