瀏覽代碼

预约明细

zhangjie 1 年之前
父節點
當前提交
77c77de4b8

+ 59 - 0
src/api/order.ts

@@ -1,11 +1,26 @@
 import axios from 'axios';
 import type {
+  OptionItem,
   TaskListPageRes,
   TaskItemDetail,
   TaskRuleUpdateParams,
+  OrderRecordListPageRes,
+  OrderRecordListPageParam,
 } from './types/order';
 import { AbleParams } from './types/common';
 
+// common
+export function teachingQuery(): Promise<OptionItem[]> {
+  return axios.post('/api/admin/apply/teaching/list', {});
+}
+export function agentQuery(teachingId: number): Promise<OptionItem[]> {
+  return axios.post(
+    '/api/admin/apply/agent/list',
+    {},
+    { params: { id: teachingId } }
+  );
+}
+
 // task-manage
 export function taskListPage(params: {
   name: string;
@@ -23,3 +38,47 @@ export function updateTaskRule(
 export function ableTask(params: AbleParams): Promise<boolean> {
   return axios.post('/api/admin/apply/task/enable', params);
 }
+
+// 预约名单
+// 预约名单详情分页
+export function orderRecordListPage(
+  params: OrderRecordListPageParam
+): Promise<OrderRecordListPageRes> {
+  return axios.post('/api/admin/apply/std/query', {}, { params });
+}
+// 预约名单详情-取消预约
+export function orderRecordCancel(apptId: number): Promise<boolean> {
+  return axios.post('/api/admin/appt/std/cancel', {}, { params: { apptId } });
+}
+// 预约名单详情-一键自动分配
+export function orderRecordAutoAssign(): Promise<boolean> {
+  return axios.post('/api/admin/apply/std/auto/assign', {});
+}
+// 预约名单详情-导出
+export function orderRecordExport(params: {
+  teachingId: number;
+  agentId: number;
+}): Promise<Blob> {
+  return axios.post(
+    '/api/admin/apply/std/export',
+    {},
+    {
+      responseType: 'blob',
+      params,
+    }
+  );
+}
+// 预约名单详情-打印签到表
+export function orderRecordPrint(params: {
+  teachingId: number;
+  agentId: number;
+}): Promise<Blob> {
+  return axios.post(
+    '/api/admin/apply/std/sign/in/print',
+    {},
+    {
+      responseType: 'blob',
+      params,
+    }
+  );
+}

+ 27 - 0
src/api/types/order.ts

@@ -1,5 +1,10 @@
 import { PageResult, PageParams } from './common';
 
+export interface OptionItem {
+  id: string;
+  name: string;
+}
+
 export interface TaskItem {
   id: string;
   name: string;
@@ -42,3 +47,25 @@ export interface TaskRuleUpdateParams {
   openApplyStartTime: number;
   openApplyEndTime: number;
 }
+
+export interface OrderRecordItem {
+  id: string;
+  stdName: string;
+  identityNumber: string;
+  studentCode: string;
+  teachingName: string;
+  agentName: string; //未预约的时候为空
+  applyTimePeriod: string; //未预约的时候为空
+  roomName: string; //未排考为空
+  seatNumber: string; //未排考为空
+  operationTime: string; //未预约的时候为空
+}
+export type OrderRecordListPageRes = PageResult<OrderRecordItem>;
+export interface OrderRecordListFilter {
+  teachingId: number;
+  agentId: number;
+  name: string;
+  identityNumber: string;
+  studentCode: string;
+}
+export type OrderRecordListPageParam = PageParams<OrderRecordListFilter>;

+ 3 - 0
src/assets/style/base.less

@@ -171,6 +171,9 @@
 .tips-dark {
   color: var(--color-text-gray);
 }
+.tips-success {
+  color: var(--color-success);
+}
 .tips-error {
   color: var(--color-danger);
 }

+ 5 - 4
src/components/file-upload/index.vue

@@ -17,6 +17,7 @@
       :custom-request="upload"
       :disabled="disabled"
       :on-before-upload="handleBeforeUpload"
+      :limit="1"
       @change="handleFileChange"
       @error="handleError"
       @success="handleSuccess"
@@ -52,7 +53,7 @@
       format?: string[];
       uploadData?: Record<string, any>;
       maxSize?: number;
-      uploadFilename?: string;
+      uploadFileAlias?: string;
       autoUpload?: boolean;
       disabled?: boolean;
     }>(),
@@ -63,7 +64,7 @@
         return {};
       },
       maxSize: 20 * 1024 * 1024,
-      uploadFilename: 'filename',
+      uploadFileAlias: 'file',
       autoUpload: true,
       disabled: false,
     }
@@ -104,7 +105,7 @@
   async function handleBeforeUpload(file: File) {
     uploadDataDict.value = {
       ...props.uploadData,
-      [props.uploadFilename]: file.name,
+      filename: file.name,
     };
 
     if (file.size > props.maxSize) {
@@ -132,7 +133,7 @@
     Object.entries(data).forEach(([k, v]) => {
       formData.append(k, v);
     });
-    formData.append('file', fileItem.file);
+    formData.append(props.uploadFileAlias, fileItem.file);
     emit('uploading');
 
     const res = await axios

+ 342 - 0
src/components/import-dialog/index.vue

@@ -0,0 +1,342 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    :width="500"
+    :title="title"
+    @before-open="modalBeforeOpen"
+  >
+    <slot></slot>
+    <div class="import-box">
+      <div class="import-temp">
+        <span>模板下载:</span>
+        <div class="temp-btn">
+          <a v-if="downloadUrl" :href="downloadUrl" :download="dfilename">{{
+            dfilename
+          }}</a>
+          <a-button
+            v-else-if="downloadHandle"
+            type="text"
+            @click="downloadHandle"
+            >{{ dfilename }}</a-button
+          >
+        </div>
+      </div>
+      <div class="import-body">
+        <a-upload
+          v-if="visible"
+          ref="uploadRef"
+          draggable
+          :action="uploadUrl"
+          :headers="headers"
+          :max-size="maxSize"
+          :format="format"
+          :accept="accept"
+          :data="uploadDataDict"
+          :show-file-list="true"
+          :auto-upload="autoUpload"
+          :custom-request="upload"
+          :disabled="disabled"
+          :on-before-upload="handleBeforeUpload"
+          :file-list="customFileList"
+          :show-remove-button="false"
+          :show-retry-button="false"
+          @change="handleFileChange"
+          @error="handleError"
+          @success="handleSuccess"
+        >
+        </a-upload>
+        <p
+          v-if="result.message"
+          :class="[
+            `tips-info`,
+            {
+              'tips-success': result.success,
+              'tips-error': !result.success,
+            },
+          ]"
+        >
+          {{ result.message }}
+        </p>
+      </div>
+    </div>
+
+    <template v-if="!autoUpload" #footer>
+      <a-button
+        type="primary"
+        :disabled="loading || !canUpload"
+        @click="confirm"
+        >确认</a-button
+      >
+      <a-button @click="close">取消</a-button>
+    </template>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+  import { computed, ref } from 'vue';
+  import { fileMD5 } from '@/utils/md5';
+  import { FileItem, Message, RequestOption } from '@arco-design/web-vue';
+  import axios from 'axios';
+
+  import useModal from '@/hooks/modal';
+
+  defineOptions({
+    name: 'ImportDialog',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  interface PromiseFunc {
+    (): Promise<boolean>;
+  }
+
+  const props = withDefaults(
+    defineProps<{
+      title: string;
+      uploadUrl: string;
+      format?: string[];
+      uploadData?: Record<string, any>;
+      maxSize?: number;
+      uploadFilename?: string;
+      autoUpload?: boolean;
+      disabled?: boolean;
+      uploadFileAlias?: string;
+      downloadUrl?: string;
+      downloadFilename?: string;
+      downloadHandle?: PromiseFunc;
+      beforeSubmitHandle?: PromiseFunc;
+    }>(),
+    {
+      title: '文件上传',
+      uploadUrl: '',
+      format: () => ['xls', 'xlsx'],
+      uploadData: () => {
+        return {};
+      },
+      maxSize: 20 * 1024 * 1024,
+      uploadFileAlias: 'file',
+      autoUpload: true,
+      disabled: false,
+    }
+  );
+
+  const emit = defineEmits([
+    'uploading',
+    'uploadError',
+    'uploadSuccess',
+    'validError',
+  ]);
+
+  const uploadRef = ref(null);
+  const canUpload = ref(false);
+  const uploadDataDict = ref({ md5: '' });
+  const headers = ref({});
+  const result = ref({ success: true, message: '' });
+  const loading = ref(false);
+  const customFileList = ref<FileItem[]>([]);
+
+  const dfilename = computed(() => {
+    if (props.downloadFilename) return props.downloadFilename;
+    return props.downloadUrl ? props.downloadUrl.split('/').pop() : '';
+  });
+  const accept = computed(() => {
+    return props.format.map((el) => `.${el}`).join();
+  });
+
+  async function confirm() {
+    loading.value = true;
+    if (props.beforeSubmitHandle) {
+      let handleResult = true;
+      await props.beforeSubmitHandle().catch(() => {
+        handleResult = false;
+      });
+      if (!handleResult) return;
+    }
+    uploadRef.value?.submit();
+  }
+
+  function checkFileFormat(fileType) {
+    const fileFormat = fileType.split('.').pop().toLocaleLowerCase();
+    return props.format.some((item) => item.toLocaleLowerCase() === fileFormat);
+  }
+
+  function handleFileChange(fileList: FileItem[]) {
+    customFileList.value = fileList.length ? fileList.slice(-1) : [];
+    const lastUid = customFileList.value[0]?.uid;
+    if (lastUid !== fileList[0].uid) {
+      result.value = {
+        success: true,
+        message: '',
+      };
+    }
+
+    if (props.autoUpload) return;
+
+    canUpload.value = customFileList.value[0]?.status === 'init';
+  }
+
+  async function handleBeforeUpload(file: File) {
+    uploadDataDict.value = {
+      ...props.uploadData,
+      filename: file.name,
+    };
+
+    if (file.size > props.maxSize) {
+      handleExceededSize();
+      return Promise.reject(result.value);
+    }
+
+    if (!checkFileFormat(file.name)) {
+      handleFormatError();
+      return Promise.reject(result.value);
+    }
+
+    const md5 = await fileMD5(file);
+    headers.value.md5 = md5;
+
+    if (props.autoUpload) loading.value = true;
+
+    return true;
+  }
+
+  async function upload(option: RequestOption) {
+    const { onProgress, onError, onSuccess, fileItem, data } = option;
+
+    const formData = new FormData();
+    Object.entries(data).forEach(([k, v]) => {
+      formData.append(k, v);
+    });
+    formData.append(props.uploadFileAlias, fileItem.file);
+    emit('uploading');
+
+    const res = await axios
+      .post(option.action, formData, {
+        headers: option.headers,
+        onUploadProgress: ({ loaded, total }) => {
+          onProgress(Math.floor((100 * loaded) / total));
+        },
+      })
+      .catch((error: Error) => {
+        onError(error);
+      });
+    if (!res) return;
+    onSuccess(res);
+  }
+
+  function handleError(error: Error) {
+    console.log(error);
+
+    canUpload.value = false;
+    loading.value = false;
+    result.value = {
+      success: false,
+      message: '上传失败',
+    };
+    emit('uploadError', result.value);
+  }
+  function handleSuccess(fileItem: FileItem) {
+    canUpload.value = false;
+    loading.value = false;
+    result.value = {
+      success: true,
+      message: '上传成功!',
+    };
+    Message.success('上传成功!');
+    emit('uploadSuccess', {
+      ...result.value,
+      filename: fileItem.name,
+      response: fileItem.response,
+    });
+  }
+
+  function handleFormatError() {
+    const content = `只支持文件格式为${props.format.join('/')}`;
+    result.value = {
+      success: false,
+      message: content,
+    };
+    loading.value = false;
+    Message.error(content);
+    emit('validError', result.value);
+  }
+  function handleExceededSize() {
+    const content = `文件大小不能超过${Math.floor(props.maxSize / 1024)}M`;
+    result.value = {
+      success: false,
+      message: content,
+    };
+    loading.value = false;
+    Message.error(content);
+    emit('validError', result.value);
+  }
+
+  function modalBeforeOpen() {
+    canUpload.value = false;
+    result.value = {
+      success: true,
+      message: '',
+    };
+    headers.value = { md5: '' };
+    loading.value = false;
+    uploadDataDict.value = {};
+  }
+</script>
+
+<style lang="less">
+  .import-box {
+    .import-temp {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 10px;
+
+      > span {
+        flex-grow: 0;
+        flex-shrink: 0;
+        line-height: 20px;
+        display: block;
+      }
+
+      .temp-btn {
+        flex-grow: 2;
+        text-align: left;
+
+        > a {
+          flex-grow: 2;
+          line-height: 20px;
+          color: var(--color-primary);
+
+          &:hover {
+            text-decoration: underline;
+            opacity: 0.8;
+          }
+        }
+      }
+      .arco-btn {
+        line-height: 20px;
+        height: auto;
+        padding: 0;
+        background: transparent;
+
+        &:hover {
+          text-decoration: underline;
+          opacity: 0.8;
+        }
+      }
+    }
+
+    .arco-upload-list-item {
+      margin-top: 8px !important;
+    }
+    .arco-upload-progress {
+      display: none;
+    }
+
+    .tips-info {
+      max-height: 100px;
+      overflow: hidden;
+      margin-top: 5px;
+    }
+  }
+</style>

+ 4 - 2
src/components/index.ts

@@ -1,12 +1,14 @@
 import { App } from 'vue';
 
 // selection
-import SelectOrg from './select-org/index.vue';
+import SelectTeaching from './select-teaching/index.vue';
+import SelectAgent from './select-agent/index.vue';
 import SelectRangeDatetime from './select-range-datetime/index.vue';
 
 export default {
   install(Vue: App) {
-    Vue.component('SelectOrg', SelectOrg);
+    Vue.component('SelectTeaching', SelectTeaching);
+    Vue.component('SelectAgent', SelectAgent);
     Vue.component('SelectRangeDatetime', SelectRangeDatetime);
   },
 };

+ 80 - 0
src/components/select-agent/index.vue

@@ -0,0 +1,80 @@
+<template>
+  <a-select
+    v-model="selected"
+    :placeholder="placeholder"
+    :allow-clear="clearable"
+    :disabled="disabled"
+    :options="optionList"
+    filter-option
+    v-bind="attrs"
+    @change="onChange"
+  >
+  </a-select>
+</template>
+
+<script setup lang="ts">
+  import { ref, useAttrs, watch } from 'vue';
+  import { agentQuery } from '@/api/order';
+
+  defineOptions({
+    name: 'SelectAgent',
+  });
+
+  const props = defineProps<{
+    modelValue: string;
+    clearable?: boolean;
+    disabled?: boolean;
+    placeholder?: string;
+    multiple?: boolean;
+    teachingId: number;
+  }>();
+  const emit = defineEmits(['update:modelValue', 'change']);
+  const attrs = useAttrs();
+
+  interface OptionListItem {
+    value: string;
+    label: string;
+  }
+
+  const selected = ref('');
+  const optionList = ref<OptionListItem[]>([]);
+  const search = async () => {
+    optionList.value = [];
+    if (!props.teachingId) return;
+
+    const resData = await agentQuery(props.teachingId);
+
+    optionList.value = (resData || []).map((item) => {
+      return { ...item, value: item.id, label: item.name };
+    });
+  };
+
+  const onChange = () => {
+    const selectedData = props.multiple
+      ? optionList.value.filter(
+          (item) => selected.value && selected.value.includes(item.value)
+        )
+      : optionList.value.filter((item) => selected.value === item.value);
+    emit('update:modelValue', selected.value);
+    emit('change', props.multiple ? selectedData : selectedData[0]);
+  };
+
+  watch(
+    () => props.modelValue,
+    (val) => {
+      selected.value = val;
+    },
+    {
+      immediate: true,
+    }
+  );
+  watch(
+    () => props.teachingId,
+    () => {
+      search();
+    },
+    {
+      immediate: true,
+    }
+  );
+</script>

+ 0 - 77
src/components/select-org/index.vue

@@ -1,77 +0,0 @@
-<template>
-  <a-tree-select
-    v-model="selected"
-    :placeholder="placeholder"
-    :allow-clear="clearable"
-    :disabled="disabled"
-    :data="optionList"
-    :field-names="fieldNames"
-    @change="onChange"
-  >
-  </a-tree-select>
-</template>
-
-<script setup lang="ts">
-  import { ref, watch } from 'vue';
-  import { omit } from 'lodash';
-  import { organizationList } from '@/api/base';
-  import { OrgTreeItem, OrgItem } from '@/api/types/base';
-
-  defineOptions({
-    name: 'SelectOrg',
-  });
-
-  const props = defineProps<{
-    modelValue: string;
-    clearable?: boolean;
-    disabled?: boolean;
-    placeholder?: string;
-    multiple?: boolean;
-  }>();
-  const emit = defineEmits(['update:modelValue', 'change']);
-
-  const fieldNames = {
-    key: 'id',
-    title: 'name',
-  };
-
-  const selected = ref('');
-  const optionList = ref<OrgTreeItem[]>([]);
-  const search = async () => {
-    optionList.value = [];
-    const resData = await organizationList();
-    optionList.value = resData || [];
-  };
-  search();
-
-  const getDataByIds = (ids: string[]): OrgItem[] => {
-    const datas: OrgItem[] = [];
-    const getData = (list: OrgTreeItem[]) => {
-      list.forEach((item) => {
-        if (ids.includes(item.id)) {
-          datas.push(omit(item, ['children']));
-        }
-      });
-    };
-    getData(optionList.value);
-    return datas;
-  };
-
-  const onChange = () => {
-    const selectedData = props.multiple
-      ? getDataByIds(selected.value)
-      : getDataByIds([selected.value]);
-    emit('update:modelValue', selected.value);
-    emit('change', props.multiple ? selectedData : selectedData[0]);
-  };
-
-  watch(
-    () => props.modelValue,
-    (val) => {
-      selected.value = val;
-    },
-    {
-      immediate: true,
-    }
-  );
-</script>

+ 69 - 0
src/components/select-teaching/index.vue

@@ -0,0 +1,69 @@
+<template>
+  <a-select
+    v-model="selected"
+    :placeholder="placeholder"
+    :allow-clear="clearable"
+    :disabled="disabled"
+    :options="optionList"
+    filter-option
+    v-bind="attrs"
+    @change="onChange"
+  >
+  </a-select>
+</template>
+
+<script setup lang="ts">
+  import { ref, useAttrs, watch } from 'vue';
+  import { teachingQuery } from '@/api/order';
+
+  defineOptions({
+    name: 'SelectTeaching',
+  });
+
+  const props = defineProps<{
+    modelValue: string;
+    clearable?: boolean;
+    disabled?: boolean;
+    placeholder?: string;
+    multiple?: boolean;
+  }>();
+  const emit = defineEmits(['update:modelValue', 'change']);
+  const attrs = useAttrs();
+
+  interface OptionListItem {
+    value: string;
+    label: string;
+  }
+
+  const selected = ref('');
+  const optionList = ref<OptionListItem[]>([]);
+  const search = async () => {
+    optionList.value = [];
+    const resData = await teachingQuery();
+
+    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.includes(item.value)
+        )
+      : optionList.value.filter((item) => selected.value === item.value);
+    emit('update:modelValue', selected.value);
+    emit('change', props.multiple ? selectedData : selectedData[0]);
+  };
+
+  watch(
+    () => props.modelValue,
+    (val) => {
+      selected.value = val;
+    },
+    {
+      immediate: true,
+    }
+  );
+</script>

+ 1 - 1
src/mock/datas/user.ts

@@ -29,7 +29,7 @@ export const menus = [
   {
     id: '4',
     name: '预约名单详情',
-    url: 'OrderRecodeManage',
+    url: 'OrderRecordManage',
     type: 'MENU',
     parentId: '1',
     sequence: 3,

+ 19 - 0
src/mock/task.ts

@@ -20,5 +20,24 @@ setupMock({
         },
       ]);
     });
+
+    // 预约名单
+    // 预约名单详情分页
+    Mock.mock(new RegExp('/api/admin/apply/std/query'), () => {
+      return pageListResponseWrap([
+        {
+          id: 1,
+          stdName: '张三',
+          identityNumber: '0120553551541',
+          studentCode: '123456789',
+          teachingName: '东莞学习中心',
+          agentName: 'A考点', // 未预约的时候为空
+          applyTimePeriod: '2024-03-01 08:00-12:00', // 未预约的时候为空
+          roomName: '考场', // 未排考为空
+          seatNumber: '1-1', // 未排考为空
+          operationTime: Date.now(), // 未预约的时候为空
+        },
+      ]);
+    });
   },
 });

+ 3 - 3
src/router/routes/modules/order.ts

@@ -28,9 +28,9 @@ const ORDER: AppRouteRecordRaw = {
       },
     },
     {
-      path: 'order-recode-manage',
-      name: 'OrderRecodeManage',
-      component: () => import('@/views/order/order-recode-manage/index.vue'),
+      path: 'order-record-manage',
+      name: 'OrderRecordManage',
+      component: () => import('@/views/order/order-record-manage/index.vue'),
       meta: {
         title: '预约名单详情',
         requiresAuth: true,

+ 252 - 0
src/views/order/order-record-manage/index.vue

@@ -0,0 +1,252 @@
+<template>
+  <div class="part-box is-filter">
+    <a-form
+      layout="inline"
+      :model="searchModel"
+      :label-col-props="{ span: 0, offset: 0 }"
+      :wrapper-col-props="{ span: 24, offset: 0 }"
+    >
+      <a-form-item label="教学点">
+        <SelectTeaching
+          v-model="searchModel.teachingId"
+          placeholder="教学点"
+          allow-clear
+        />
+      </a-form-item>
+      <a-form-item label="考点">
+        <SelectAgent
+          v-model="searchModel.agentId"
+          placeholder="考点"
+          allow-clear
+          :teaching-id="searchModel.teachingId"
+        />
+      </a-form-item>
+      <a-form-item label="姓名">
+        <a-input
+          v-model.trim="searchModel.name"
+          placeholder="姓名"
+          allow-clear
+        ></a-input>
+      </a-form-item>
+      <a-form-item label="证件号">
+        <a-input
+          v-model.trim="searchModel.identityNumber"
+          placeholder="证件号"
+          allow-clear
+        ></a-input>
+      </a-form-item>
+      <a-form-item label="学号">
+        <a-input
+          v-model.trim="searchModel.studentCode"
+          placeholder="学号"
+          allow-clear
+        ></a-input>
+      </a-form-item>
+      <a-form-item>
+        <a-button type="primary" @click="toPage(1)">查询</a-button>
+      </a-form-item>
+    </a-form>
+    <div>
+      <a-button type="primary" :loading="exportLoading" @click="toExport"
+        >导出</a-button
+      >
+      <a-button type="primary" @click="toImport">导入预考</a-button>
+      <a-button type="primary" :loading="assginLoading" @click="toAssgin"
+        >一键自动分配</a-button
+      >
+      <a-button type="primary" :loading="printLoading" @click="toPrint"
+        >打印签到表</a-button
+      >
+    </div>
+  </div>
+  <div class="part-box">
+    <a-table :columns="columns" :data="dataList" :pagination="pagination">
+      <template #operationTime="{ record }">
+        {{ timestampFilter(record.operationTime) }}
+      </template>
+      <template #action="{ record }">
+        <a-button type="text" class="btn-primary" @click="toCancel(record)"
+          >取消</a-button
+        >
+      </template>
+    </a-table>
+
+    <!-- ImportDialog -->
+    <ImportDialog
+      ref="importStudentDialog"
+      title="导入预考"
+      upload-url="/api/admin/sys/user/import"
+      :format="['xls', 'xlsx']"
+      download-url="12312"
+      download-filename="学生预约导入模板.xlsx"
+      :auto-upload="false"
+      @upload-success="getList"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref } from 'vue';
+  import { Message, TableColumnData } from '@arco-design/web-vue';
+  import {
+    orderRecordCancel,
+    orderRecordAutoAssign,
+    orderRecordListPage,
+    orderRecordPrint,
+    orderRecordExport,
+  } from '@/api/order';
+  import { OrderRecordItem } from '@/api/types/order';
+  import useTable from '@/hooks/table';
+  // import useDictOption from '@/hooks/dict-option';
+  import useLoading from '@/hooks/loading';
+  import { timestampFilter } from '@/utils/filter';
+  import { modalConfirm } from '@/utils/arco';
+  import { downloadByApi } from '@/utils/download';
+
+  import ImportDialog from '@/components/import-dialog/index.vue';
+
+  defineOptions({
+    name: 'OrderRecordManage',
+  });
+
+  // const { getLabel: getAbleLabel } = useDictOption('ABLE_TYPE');
+
+  const searchModel = reactive({
+    teachingId: '',
+    agentId: '',
+    name: '',
+    identityNumber: '',
+    studentCode: '',
+  });
+
+  const columns: TableColumnData[] = [
+    {
+      title: '姓名',
+      dataIndex: 'stdName',
+    },
+    {
+      title: '证件号',
+      dataIndex: 'identityNumber',
+    },
+    {
+      title: '学号',
+      dataIndex: 'studentCode',
+    },
+    {
+      title: '教学点',
+      dataIndex: 'teachingName',
+    },
+    {
+      title: '考点',
+      dataIndex: 'agentName',
+    },
+    {
+      title: '预约时段',
+      dataIndex: 'applyTimePeriod',
+      width: 200,
+    },
+    {
+      title: '考场',
+      dataIndex: 'roomName',
+    },
+    {
+      title: '座位号',
+      dataIndex: 'seatNumber',
+    },
+    {
+      title: '状态',
+      dataIndex: 'seatNumber',
+    },
+    {
+      title: '操作时间',
+      dataIndex: 'operationTime',
+      slotName: 'operationTime',
+      width: 170,
+    },
+    {
+      title: '操作人',
+      dataIndex: 'operationUser',
+    },
+    {
+      title: '操作',
+      slotName: 'action',
+      width: 100,
+      fixed: 'right',
+      cellClass: 'action-column',
+    },
+  ];
+  const { dataList, pagination, toPage, getList } = useTable<OrderRecordItem[]>(
+    orderRecordListPage,
+    searchModel,
+    true
+  );
+
+  // table action
+  const { loading: exportLoading, setLoading: setExportLoading } = useLoading();
+  async function toExport() {
+    if (exportLoading.value) return;
+    setExportLoading(true);
+
+    const res = await downloadByApi(() =>
+      orderRecordExport({
+        teachingId: searchModel.teachingId,
+        agentId: searchModel.agentId,
+      })
+    ).catch((e) => {
+      Message.error(e || '下载失败,请重新尝试!');
+    });
+    setExportLoading(false);
+    if (!res) return;
+    Message.success('下载成功!');
+  }
+  // 导入
+  const importStudentDialog = ref(null);
+  function toImport() {
+    importStudentDialog.value?.open();
+  }
+
+  // 一键分配
+  const { loading: assginLoading, setLoading: setAssignLoading } = useLoading();
+  async function toAssgin() {
+    if (assginLoading.value) return;
+    setAssignLoading(true);
+
+    const res = await orderRecordAutoAssign().catch(() => false);
+    setAssignLoading(false);
+    if (!res) return;
+    Message.success('操作成功!');
+    getList();
+  }
+
+  // 打印签到表
+  const { loading: printLoading, setLoading: setPrintLoading } = useLoading();
+  async function toPrint() {
+    if (printLoading.value) return;
+    setPrintLoading(true);
+
+    const res = await downloadByApi(() =>
+      orderRecordPrint({
+        teachingId: searchModel.teachingId,
+        agentId: searchModel.agentId,
+      })
+    ).catch((e) => {
+      Message.error(e || '下载失败,请重新尝试!');
+    });
+    setPrintLoading(false);
+    if (!res) return;
+    Message.success('下载成功!');
+  }
+
+  // 取消
+  async function toCancel(row: OrderRecordItem) {
+    const confirmRes = await modalConfirm(
+      '提示',
+      `确定要取消【${row.stdName}】的预约吗?`
+    ).catch(() => false);
+    if (confirmRes !== 'confirm') return;
+
+    await orderRecordCancel(row.id);
+    Message.success('操作成功!');
+    getList();
+  }
+</script>

+ 0 - 0
src/views/order/order-recode-manage/index.vue → src/views/order/student-import/index.vue