Browse Source

feat: 轨迹导出页面初步

zhangjie 1 year ago
parent
commit
f9c2b37138

+ 8 - 0
electron/main/useElectron.ts

@@ -7,6 +7,14 @@ function handleDialogSelectFile(
   return dialog.showOpenDialog(config);
 }
 
+function handleDialogSaveFile(
+  event: Electron.IpcMainInvokeEvent,
+  config: Electron.SaveDialogOptions
+) {
+  return dialog.showSaveDialog(config);
+}
+
 export default function useElectron() {
   ipcMain.handle('dialog:selectFile', handleDialogSelectFile);
+  ipcMain.handle('dialog:saveFile', handleDialogSaveFile);
 }

+ 7 - 0
electron/preload/apiElectron.ts

@@ -6,8 +6,15 @@ function dialogSelectFile(
   return ipcRenderer.invoke('dialog:selectFile', config);
 }
 
+function dialogSaveFile(
+  config: Electron.SaveDialogOptions
+): Promise<Electron.SaveDialogReturnValue> {
+  return ipcRenderer.invoke('dialog:saveFile', config);
+}
+
 const electronApi = {
   dialogSelectFile,
+  dialogSaveFile,
 };
 
 export type ElectronApi = typeof electronApi;

+ 22 - 1
src/api/task.ts

@@ -1,5 +1,26 @@
 import axios from 'axios';
-import { CardData, Task, StudentObjectiveInfo } from './types/task';
+import {
+  CardData,
+  Task,
+  StudentObjectiveInfo,
+  SemesterItem,
+  ExamItem,
+} from './types/task';
+
+// semester
+export function semesterQuery(): Promise<SemesterItem[]> {
+  return axios.post('/api/admin/basic/condition/list_semester', {});
+}
+// exam
+export function examQuery(semesterId: string): Promise<ExamItem[]> {
+  return axios.post(
+    '/api/admin/basic/condition/list_exam',
+    {},
+    {
+      params: { semesterId },
+    }
+  );
+}
 
 /** 查看单个学生的试卷轨迹 */
 export async function getSingleStudentTaskOfStudentTrack(

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

@@ -1,3 +1,12 @@
+export interface SemesterItem {
+  id: string;
+  name: string;
+}
+export interface ExamItem {
+  id: string;
+  name: string;
+}
+
 export interface CardData {
   id: string;
   content: string;

+ 6 - 0
src/components/index.ts

@@ -8,6 +8,9 @@ import SelectRangeDatetime from './select-range-datetime/index.vue';
 import SelectRangeTime from './select-range-time/index.vue';
 import StatusTag from './status-tag/index.vue';
 
+import SelectExam from './select-exam/index.vue';
+import SelectSemester from './select-semester/index.vue';
+
 export default {
   install(Vue: App) {
     Vue.component('ImportDialog', ImportDialog);
@@ -16,5 +19,8 @@ export default {
     Vue.component('SelectRangeDatetime', SelectRangeDatetime);
     Vue.component('SelectRangeTime', SelectRangeTime);
     Vue.component('StatusTag', StatusTag);
+
+    Vue.component('SelectExam', SelectExam);
+    Vue.component('SelectSemester', SelectSemester);
   },
 };

+ 72 - 0
src/components/select-exam/index.vue

@@ -0,0 +1,72 @@
+<template>
+  <a-select
+    v-model="selected"
+    :placeholder="placeholder"
+    :allow-clear="clearable"
+    :disabled="disabled"
+    :options="optionList"
+    allow-search
+    popup-container="body"
+    v-bind="attrs"
+    @change="onChange"
+  >
+    <template v-if="prefix" #prefix>考试</template>
+  </a-select>
+</template>
+
+<script setup lang="ts">
+  import { ref, useAttrs, watch } from 'vue';
+  import { examQuery } from '@/api/task';
+  import { OptionListItem } from '@/types/global';
+
+  defineOptions({
+    name: 'SelectExam',
+  });
+  type ValueType = string | Array<string> | null;
+
+  const props = defineProps<{
+    modelValue: ValueType;
+    clearable?: boolean;
+    disabled?: boolean;
+    placeholder?: string;
+    multiple?: boolean;
+    prefix?: boolean;
+    semesterId: string | null;
+  }>();
+  const emit = defineEmits(['update:modelValue', 'change']);
+  const attrs = useAttrs();
+
+  const selected = ref<string | Array<string> | undefined>();
+  const optionList = ref<OptionListItem[]>([]);
+  const search = async () => {
+    optionList.value = [];
+    if (!props.semesterId) return;
+    const resData = await examQuery(props.semesterId);
+
+    optionList.value = (resData || []).map((item) => {
+      return { ...item, value: item.id, label: item.name };
+    });
+  };
+  search();
+
+  const onChange = () => {
+    const selectedData = props.multiple
+      ? optionList.value.filter(
+          (item) =>
+            selected.value && (selected.value as string[]).includes(item.value)
+        )
+      : optionList.value.filter((item) => selected.value === item.value);
+    emit('update:modelValue', selected.value || null);
+    emit('change', props.multiple ? selectedData : selectedData[0]);
+  };
+
+  watch(
+    () => props.modelValue,
+    (val) => {
+      selected.value = val || undefined;
+    },
+    {
+      immediate: true,
+    }
+  );
+</script>

+ 70 - 0
src/components/select-semester/index.vue

@@ -0,0 +1,70 @@
+<template>
+  <a-select
+    v-model="selected"
+    :placeholder="placeholder"
+    :allow-clear="clearable"
+    :disabled="disabled"
+    :options="optionList"
+    allow-search
+    popup-container="body"
+    v-bind="attrs"
+    @change="onChange"
+  >
+    <template v-if="prefix" #prefix>学期</template>
+  </a-select>
+</template>
+
+<script setup lang="ts">
+  import { ref, useAttrs, watch } from 'vue';
+  import { semesterQuery } from '@/api/task';
+  import { OptionListItem } from '@/types/global';
+
+  defineOptions({
+    name: 'SelectSemester',
+  });
+  type ValueType = string | Array<string> | null;
+
+  const props = defineProps<{
+    modelValue: ValueType;
+    clearable?: boolean;
+    disabled?: boolean;
+    placeholder?: string;
+    multiple?: boolean;
+    prefix?: boolean;
+  }>();
+  const emit = defineEmits(['update:modelValue', 'change']);
+  const attrs = useAttrs();
+
+  const selected = ref<string | Array<string> | undefined>();
+  const optionList = ref<OptionListItem[]>([]);
+  const search = async () => {
+    optionList.value = [];
+    const resData = await semesterQuery();
+
+    optionList.value = (resData || []).map((item) => {
+      return { ...item, value: item.id, label: item.name };
+    });
+  };
+  search();
+
+  const onChange = () => {
+    const selectedData = props.multiple
+      ? optionList.value.filter(
+          (item) =>
+            selected.value && (selected.value as string[]).includes(item.value)
+        )
+      : optionList.value.filter((item) => selected.value === item.value);
+    emit('update:modelValue', selected.value || null);
+    emit('change', props.multiple ? selectedData : selectedData[0]);
+  };
+
+  watch(
+    () => props.modelValue,
+    (val) => {
+      selected.value = val || undefined;
+    },
+    {
+      immediate: true,
+    }
+  );
+</script>

+ 5 - 5
src/components/status-tag/index.vue

@@ -4,17 +4,17 @@
 
 <script setup lang="ts">
   import { computed } from 'vue';
-  import useDictOption from '@/hooks/dict-option';
+  // import useDictOption from '@/hooks/dict-option';
 
   defineOptions({
     name: 'StatusTag',
   });
 
   const configs = {
-    enable: {
-      themeDict: { true: 'green', false: 'red' },
-      valFilter: useDictOption('ABLE_TYPE').getLabel,
-    },
+    // enable: {
+    //   themeDict: { true: 'green', false: 'red' },
+    //   valFilter: useDictOption('ABLE_TYPE').getLabel,
+    // },
   };
   type ConfigKeyType = keyof typeof configs;
 

+ 11 - 49
src/constants/enumerate.ts

@@ -1,5 +1,3 @@
-export const SYS_ADMIN_NAME = 'sysadmin';
-
 export const DEFAULT_LABEL = '--';
 
 // 通用 -------------->
@@ -10,55 +8,19 @@ export const ABLE_TYPE = {
 };
 
 // 基础 -------------->
-// 机构
-export const ORG_TYPE = {
-  PRINTING_HOUSE: '印刷室',
-};
-// 角色
-export const ROLE_TYPE = {
-  SCHOOL_ADMIN: '管理员',
-  EXAM_TEACHER: '考务老师',
-  QUESTION_TEACHER: '命题老师',
-  CUSTOMER: '客服人员',
-  PRINT: '印刷人员',
-  CUSTOM: '自定义',
-};
-export const SMS_TYPE = {
-  SCHOOL_ADMIN: '管理员',
-  EXAM_TEACHER: '考务老师',
-};
-export const DATA_PRIVILEGE_TYPE = {
-  SELF: '仅本人数据权限',
-  SELF_ORG: '本部门数据权限',
-  SELF_ORG_BELOW: '本部门及下级部门数据权限',
-  ALL: '全部数据权限',
+// 导出文件类型
+export const EXT_TYPE = {
+  img: '图片',
+  pdf: 'PDF',
 };
+export type ExtTypeEnum = keyof typeof EXT_TYPE;
 
-// 题卡
-// 条码类型
-export const EXAM_NUMBER_STYLE = {
-  PRINT: '印刷条码',
-  PASTE: '粘贴条码',
-  FILL: '考号填涂',
-};
-export const CARD_CREATE_METHOD_TYPE = {
-  UPLOAD: '上传文件',
-  STANDARD: '标准模式',
-  FREE: '自由模式',
-};
-export const CARD_TYPE = {
-  GENERIC: '通卡',
-  CUSTOM: '自定义专卡',
+// 导出文件类型
+export const PICTURE_TYPE = {
+  track: '轨迹图',
+  origin: '原图',
 };
 
-export const PUSH_CARD_TYPE = {
-  GENERIC: '通卡',
-  CUSTOM: '电子题卡',
-};
+export type PictureTypeEnum = keyof typeof PICTURE_TYPE;
 
-// 模板类型
-export const TEMPLATE_CLASSIFY = {
-  SIGN: '签到表',
-  PACKAGE: '卷袋贴',
-  CHECK_IN: '考试情况登记表',
-};
+export const IMAGE_SAVE_RULE = 'semester/exam/course/paperNumber';

+ 3 - 21
src/hooks/dict-option.ts

@@ -1,29 +1,11 @@
 import { ref } from 'vue';
-import {
-  DEFAULT_LABEL,
-  ORG_TYPE,
-  ABLE_TYPE,
-  ROLE_TYPE,
-  SMS_TYPE,
-  DATA_PRIVILEGE_TYPE,
-  EXAM_NUMBER_STYLE,
-  CARD_CREATE_METHOD_TYPE,
-  CARD_TYPE,
-  TEMPLATE_CLASSIFY,
-} from '@/constants/enumerate';
+import { DEFAULT_LABEL, EXT_TYPE, PICTURE_TYPE } from '@/constants/enumerate';
 import { dictToOption } from '@/utils/utils';
 import { SelectOptions } from '@/types/global';
 
 const dicts = {
-  ORG_TYPE,
-  ABLE_TYPE,
-  ROLE_TYPE,
-  SMS_TYPE,
-  DATA_PRIVILEGE_TYPE,
-  EXAM_NUMBER_STYLE,
-  CARD_CREATE_METHOD_TYPE,
-  CARD_TYPE,
-  TEMPLATE_CLASSIFY,
+  EXT_TYPE,
+  PICTURE_TYPE,
 };
 
 type DictTypeType = keyof typeof dicts;

+ 0 - 15
src/layout/default-layout.vue

@@ -6,26 +6,11 @@
     <div class="home-body">
       <router-view />
     </div>
-    <div v-if="props.showFooter" class="home-footer">
-      <div class="home-footer-ip">
-        <i class="el-icon-monitor"></i><span>{{ appStore.domain }}</span>
-      </div>
-
-      <div class="home-footer-version">版本号:{{ appStore.version }}</div>
-    </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-  import { useAppStore } from '@/store';
-
   defineOptions({
     name: 'DefaultLayout',
   });
-
-  const props = defineProps<{
-    showFooter?: boolean;
-  }>();
-
-  const appStore = useAppStore();
 </script>

+ 24 - 0
src/router/routes/modules/base.ts

@@ -0,0 +1,24 @@
+import { DEFAULT_LAYOUT } from '../base';
+import { AppRouteRecordRaw } from '../types';
+
+const routes: AppRouteRecordRaw = {
+  path: '/base',
+  name: 'base',
+  component: DEFAULT_LAYOUT,
+  meta: {
+    requiresAuth: false,
+  },
+  children: [
+    {
+      path: 'track-export',
+      name: 'TrackExport',
+      component: () => import('@/views/base/track-export/index.vue'),
+      meta: {
+        title: '轨迹图导出',
+        requiresAuth: false,
+      },
+    },
+  ],
+};
+
+export default routes;

+ 1 - 1
src/types/global.ts

@@ -41,7 +41,7 @@ export interface GeneralChart {
 export type FormRules<T extends string> = Partial<Record<T, FieldRule[]>>;
 
 export interface SelectOptions {
-  value: string | number | boolean | Record<string, unknown>;
+  value: string | number | boolean;
   label: string;
 }
 

+ 122 - 0
src/views/base/track-export/index.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="part-box">
+    <a-form ref="formRef" :model="formData" :rules="rules" auto-label-width>
+      <a-form-item field="semesterId" label="学期">
+        <!-- <SelectSemester v-model="formData.semesterId" placeholder="请选择" /> -->
+      </a-form-item>
+      <a-form-item field="examId" label="考试">
+        <!-- <SelectExam v-model="formData.examId" placeholder="请选择" /> -->
+      </a-form-item>
+      <a-form-item field="extType" label="文件类型">
+        <a-radio-group v-model="formData.extType">
+          <a-radio
+            v-for="(option, index) in extOptions"
+            :key="index"
+            :value="option.value"
+            >{{ option.label }}</a-radio
+          >
+        </a-radio-group>
+      </a-form-item>
+      <a-form-item field="pictureType" label="图片类型">
+        <a-checkbox-group v-model="formData.pictureType">
+          <a-checkbox
+            v-for="(option, index) in pictureOptions"
+            :key="index"
+            :value="option.value"
+            >{{ option.label }}</a-checkbox
+          >
+        </a-checkbox-group>
+      </a-form-item>
+      <a-form-item field="outputDir" label="保存目录">
+        <a-input-search
+          v-model.trim="formData.outputDir"
+          :style="{ width: '400px' }"
+          search-button
+          button-text="选择"
+          @search="toSelectDir"
+        >
+        </a-input-search>
+      </a-form-item>
+      <a-form-item>
+        <a-button type="primary">开始</a-button>
+      </a-form-item>
+    </a-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive } from 'vue';
+  import type { FormInstance } from '@arco-design/web-vue/es/form';
+  import { FormRules } from '@/types/global';
+  import useDictOption from '@/hooks/dict-option';
+  import { ExtTypeEnum, PictureTypeEnum } from '@/constants/enumerate';
+
+  defineOptions({
+    name: 'TrackExport',
+  });
+
+  const { optionList: extOptions } = useDictOption('EXT_TYPE');
+  const { optionList: pictureOptions } = useDictOption('PICTURE_TYPE');
+
+  const defaultFormData = {
+    semesterId: '',
+    examId: '',
+    extType: 'img' as ExtTypeEnum,
+    pictureType: ['track'] as PictureTypeEnum[],
+    outputDir: '',
+  };
+
+  interface FormDataType {
+    semesterId: string;
+    examId: string;
+    extType: ExtTypeEnum;
+    pictureType: PictureTypeEnum[];
+    outputDir: string;
+  }
+
+  const formRef = ref<FormInstance>();
+  const formData = reactive<FormDataType>({ ...defaultFormData });
+  const rules: FormRules<keyof FormDataType> = {
+    semesterId: [
+      {
+        required: true,
+        message: '请选择学期',
+      },
+    ],
+    examId: [
+      {
+        required: true,
+        message: '请选择考试',
+      },
+    ],
+    extType: [
+      {
+        required: true,
+        message: '请选择文件类型',
+      },
+    ],
+    pictureType: [
+      {
+        required: true,
+        message: '请选择图片类型',
+      },
+    ],
+    outputDir: [
+      {
+        required: true,
+        message: '请选择保存目录',
+      },
+    ],
+  };
+
+  async function toSelectDir() {
+    const result = await window.electron.dialogSelectFile({
+      title: '选择保存目录',
+      properties: ['openDirectory'],
+    });
+
+    if (result.canceled) return;
+
+    formData.outputDir = result.filePaths[0];
+  }
+</script>

+ 0 - 0
src/views/track/readme.md → src/views/base/track-export/readme.md


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

@@ -5,7 +5,7 @@ import {
 } from '@/api/task';
 import { Task, Track, SpecialTag } from '@/api/types/task';
 import { calcSum, maxNum, randomCode, strGbLen } from '@/utils/utils';
-import { DrawTrackItem } from '../../../electron/preload/types';
+import { DrawTrackItem } from '../../../../electron/preload/types';
 
 type AnswerMap = Record<string, { answer: string; isRight: boolean }>;
 

+ 0 - 9
src/views/track/index.vue

@@ -1,9 +0,0 @@
-<template>
-  <div></div>
-</template>
-
-<script setup lang="ts">
-  defineOptions({
-    name: 'TrackImg',
-  });
-</script>