浏览代码

feat: 扫描进度

zhangjie 1 周之前
父节点
当前提交
67e9e1332a

+ 1 - 0
components.d.ts

@@ -31,6 +31,7 @@ declare module '@vue/runtime-core' {
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem'];
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem'];
     ElOption: typeof import('element-plus/es')['ElOption'];
     ElOption: typeof import('element-plus/es')['ElOption'];
     ElPagination: typeof import('element-plus/es')['ElPagination'];
     ElPagination: typeof import('element-plus/es')['ElPagination'];
+    ElProgress: typeof import('element-plus/es')['ElProgress'];
     ElResult: typeof import('element-plus/es')['ElResult'];
     ElResult: typeof import('element-plus/es')['ElResult'];
     ElSelect: typeof import('element-plus/es')['ElSelect'];
     ElSelect: typeof import('element-plus/es')['ElSelect'];
     ElSpace: typeof import('element-plus/es')['ElSpace'];
     ElSpace: typeof import('element-plus/es')['ElSpace'];

+ 30 - 0
src/api/scan.ts

@@ -0,0 +1,30 @@
+import axios from 'axios';
+import {
+  ScanCousreListPageParam,
+  ScanPointListPageParam,
+  ScanListPageRes,
+  SignPaperStatListPageParam,
+  SignPaperStatListPageRes,
+} from './types/scan';
+
+// 扫描管理
+// 按科目统计列表
+export function getScanCourseList(
+  params: ScanCousreListPageParam
+): Promise<ScanListPageRes> {
+  return axios.post('/api/student/list', { params });
+}
+
+// 按考点统计列表
+export function getScanPointList(
+  params: ScanPointListPageParam
+): Promise<ScanListPageRes> {
+  return axios.post('/api/student/list', { params });
+}
+
+// 签到表统计列表
+export function getSignPaperStatList(
+  params: SignPaperStatListPageParam
+): Promise<SignPaperStatListPageRes> {
+  return axios.post('/api/student/list', { params });
+}

+ 53 - 0
src/api/types/scan.ts

@@ -0,0 +1,53 @@
+import { PageResult, PageParams } from './common';
+
+export interface ScanItem {
+  // 考试名称
+  name: string;
+  // 考生总数
+  totalStudents: number;
+  // 已扫张数
+  scannedSheets: number;
+  // 已扫人数
+  scannedStudents: number;
+  // 人工指定缺考
+  manualAbsent: number;
+  // 进度
+  progress: number;
+}
+
+export type ScanListPageRes = PageResult<ScanItem>;
+
+export interface ScanCourseListFilter {
+  // 科目
+  subject?: string;
+  // 层次
+  level?: string;
+  // 专业类型
+  majorType?: string;
+}
+export type ScanCousreListPageParam = PageParams<ScanCourseListFilter>;
+
+export interface ScanPointListFilter {
+  // 考点
+  point?: string;
+}
+export type ScanPointListPageParam = PageParams<ScanPointListFilter>;
+
+// 签到表统计项
+export interface SignPaperStatItem {
+  id: number;
+  // 签到表编号
+  signPaperNo: string;
+  // 图片数量
+  imageCount: number;
+}
+export type SignPaperStatListPageRes = PageResult<SignPaperStatItem>;
+
+export interface SignPaperStatListFilter {
+  // 签到表编号
+  signPaperNo?: string;
+  // 状态
+  status?: string;
+}
+
+export type SignPaperStatListPageParam = PageParams<SignPaperStatListFilter>;

+ 0 - 138
src/assets/style/arco-custom.less

@@ -1,138 +0,0 @@
-// arco-btn
-.arco-btn + .arco-btn {
-  margin-left: 10px;
-}
-.arco-btn-text {
-  &:not(.arco-btn-only-icon) .arco-btn-icon {
-    margin-right: 4px;
-  }
-}
-// .arco-pagination
-.arco-table {
-  .arco-table-pagination {
-    display: block;
-    margin-top: 16px;
-  }
-  .arco-pagination {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-
-    .arco-pagination-item {
-      border: 1px solid var(--color-border);
-      line-height: 30px;
-    }
-    .arco-pagination-item-active {
-      border-color: var(--color-primary);
-      background-color: var(--color-primary);
-      color: #fff;
-    }
-    .arco-select-view-single {
-      border-color: var(--color-border);
-    }
-    .arco-pagination-total {
-      flex-grow: 2;
-    }
-  }
-}
-
-// arco-table
-.action-column {
-  .arco-btn {
-    height: 20px;
-    padding: 0;
-    border: none !important;
-    outline: none !important;
-    line-height: 20px;
-    margin: 0;
-
-    &:not(:first-child) {
-      margin-left: 10px;
-    }
-    &:not(.arco-btn-disabled):hover {
-      transform: scale(1.1);
-    }
-  }
-}
-
-// arco-input
-// .arco-input-wrapper,
-// .arco-select-view-single {
-//   border-color: #d9d9d9;
-//   background-color: transparent;
-//   &:hover {
-//     border-color: #bebebe;
-//   }
-// }
-
-// arco-modal
-.arco-modal-wrapper {
-  .arco-modal {
-    border-radius: 8px;
-
-    .arco-modal-close-btn {
-      font-size: 20px;
-    }
-    .arco-icon-hover::before {
-      width: 24px;
-      height: 24px;
-      border-radius: 4px;
-    }
-    .arco-modal-header {
-      padding: 20px;
-      border-bottom: 1px solid var(--color-border);
-      margin: 0;
-      height: auto;
-      text-align: left;
-    }
-    .arco-modal-title {
-      color: var(--color-text-dark);
-    }
-    .arco-modal-close {
-      width: 24px;
-      height: 24px;
-    }
-    .arco-modal-body {
-      padding: 20px;
-    }
-    .arco-modal-footer {
-      padding: 0 20px 20px;
-      margin: 0;
-      border: none;
-
-      .arco-btn:not(:nth-child(1)) {
-        margin-left: 8px;
-      }
-    }
-
-    &.arco-modal-simple {
-      border-radius: 8px;
-      padding: 0;
-      .arco-modal-header {
-        border: none;
-        padding-bottom: 8px;
-      }
-      .arco-modal-body {
-        padding: 0 20px 20px 48px;
-      }
-      .arco-modal-footer {
-        text-align: right;
-      }
-    }
-  }
-}
-
-// .arco-select
-.arco-select-dropdown {
-  .arco-select-option {
-    &.arco-select-option-selected {
-      color: var(--color-primary);
-
-      &.arco-select-option-active {
-        &:hover {
-          color: var(--color-primary);
-        }
-      }
-    }
-  }
-}

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

@@ -98,6 +98,12 @@
     padding: 12px;
     padding: 12px;
   }
   }
 }
 }
+// page-tab
+.page-tab {
+  .el-tabs__item {
+    background-color: #fff;
+  }
+}
 
 
 // action-more
 // action-more
 .action-more {
 .action-more {

+ 137 - 0
src/assets/style/element-custom.less

@@ -0,0 +1,137 @@
+// el-btn
+.el-btn + .el-btn {
+  margin-left: 10px;
+}
+.el-btn-text {
+  &:not(.el-btn-only-icon) .el-btn-icon {
+    margin-right: 4px;
+  }
+}
+// .el-pagination
+.el-table {
+  & + .el-pagination {
+    margin-top: 10px;
+  }
+}
+.el-pagination {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  .el-pagination-item {
+    border: 1px solid var(--color-border);
+    line-height: 30px;
+  }
+  .el-pagination-item-active {
+    border-color: var(--color-primary);
+    background-color: var(--color-primary);
+    color: #fff;
+  }
+  .el-select-view-single {
+    border-color: var(--color-border);
+  }
+  .el-pagination-total {
+    flex-grow: 2;
+  }
+}
+
+// el-table
+.action-column {
+  .el-btn {
+    height: 20px;
+    padding: 0;
+    border: none !important;
+    outline: none !important;
+    line-height: 20px;
+    margin: 0;
+
+    &:not(:first-child) {
+      margin-left: 10px;
+    }
+    &:not(.el-btn-disabled):hover {
+      transform: scale(1.1);
+    }
+  }
+}
+
+// el-input
+// .el-input-wrapper,
+// .el-select-view-single {
+//   border-color: #d9d9d9;
+//   background-color: transparent;
+//   &:hover {
+//     border-color: #bebebe;
+//   }
+// }
+
+// el-modal
+.el-modal-wrapper {
+  .el-modal {
+    border-radius: 8px;
+
+    .el-modal-close-btn {
+      font-size: 20px;
+    }
+    .el-icon-hover::before {
+      width: 24px;
+      height: 24px;
+      border-radius: 4px;
+    }
+    .el-modal-header {
+      padding: 20px;
+      border-bottom: 1px solid var(--color-border);
+      margin: 0;
+      height: auto;
+      text-align: left;
+    }
+    .el-modal-title {
+      color: var(--color-text-dark);
+    }
+    .el-modal-close {
+      width: 24px;
+      height: 24px;
+    }
+    .el-modal-body {
+      padding: 20px;
+    }
+    .el-modal-footer {
+      padding: 0 20px 20px;
+      margin: 0;
+      border: none;
+
+      .el-btn:not(:nth-child(1)) {
+        margin-left: 8px;
+      }
+    }
+
+    &.el-modal-simple {
+      border-radius: 8px;
+      padding: 0;
+      .el-modal-header {
+        border: none;
+        padding-bottom: 8px;
+      }
+      .el-modal-body {
+        padding: 0 20px 20px 48px;
+      }
+      .el-modal-footer {
+        text-align: right;
+      }
+    }
+  }
+}
+
+// .el-select
+.el-select-dropdown {
+  .el-select-option {
+    &.el-select-option-selected {
+      color: var(--color-primary);
+
+      &.el-select-option-active {
+        &:hover {
+          color: var(--color-primary);
+        }
+      }
+    }
+  }
+}

+ 1 - 1
src/assets/style/index.less

@@ -1,6 +1,6 @@
 @import url('./var.less');
 @import url('./var.less');
 @import url('./reset.less');
 @import url('./reset.less');
-@import url('./arco-custom.less');
+@import url('./element-custom.less');
 @import url('./base.less');
 @import url('./base.less');
 @import url('./home.less');
 @import url('./home.less');
 @import url('./pages.less');
 @import url('./pages.less');

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

@@ -27,6 +27,15 @@ const BASE: AppRouteRecordRaw = {
         requiresAuth: true,
         requiresAuth: true,
       },
       },
     },
     },
+    {
+      path: '/scan-manage',
+      name: 'ScanManage',
+      component: () => import('@/views/scan/ScanManage.vue'),
+      meta: {
+        title: '扫描进度',
+        requiresAuth: true,
+      },
+    },
   ],
   ],
 };
 };
 
 

+ 1 - 1
src/store/modules/app/menuData.ts

@@ -69,7 +69,7 @@ export const adminMenus = [
   {
   {
     id: 5,
     id: 5,
     name: '扫描进度',
     name: '扫描进度',
-    url: 'ScanProgress',
+    url: 'ScanManage',
     type: 'MENU',
     type: 'MENU',
     parentId: -1,
     parentId: -1,
     sequence: 2,
     sequence: 2,

+ 113 - 0
src/views/scan/ScanCourseStats.vue

@@ -0,0 +1,113 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <el-select
+          v-model="searchModel.subject"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="请选择" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="层次">
+        <el-select
+          v-model="searchModel.level"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="请选择" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="专业类型">
+        <el-select
+          v-model="searchModel.majorType"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="请选择" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="exportData">导出</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column property="name" label="考试名称" min-width="200" />
+      <el-table-column
+        property="totalStudents"
+        label="考生总数"
+        min-width="100"
+      />
+      <el-table-column
+        property="scannedSheets"
+        label="已扫张数"
+        min-width="100"
+      />
+      <el-table-column
+        property="scannedStudents"
+        label="已扫人数"
+        min-width="100"
+      />
+      <el-table-column
+        property="manualAbsent"
+        label="人工指定缺考"
+        min-width="100"
+      />
+      <el-table-column label="进度" width="100">
+        <template #default="scope">
+          <el-progress
+            :percentage="scope.row.progress"
+            :color="getProgressColor(scope.row.progress)"
+          />
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      v-model:current-page="pagination.pageNumber"
+      v-model:page-size="pagination.pageSize"
+      :layout="pagination.layout"
+      :total="pagination.total"
+      @size-change="pageSizeChange"
+      @current-change="toPage"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive } from 'vue';
+  import { getScanCourseList } from '@/api/scan';
+  import { ScanItem, ScanCourseListFilter } from '@/api/types/scan';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'ScanCourseStats',
+  });
+
+  const searchModel = reactive<ScanCourseListFilter>({
+    subject: '',
+    level: '',
+    majorType: '',
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<ScanItem>(getScanCourseList, searchModel, false);
+
+  function getProgressColor(progress: number) {
+    if (progress < 30) return '#f56c6c';
+    if (progress < 70) return '#e6a23c';
+    return '#67c23a';
+  }
+
+  function exportData() {
+    // TODO: 实现导出功能
+  }
+</script>

+ 30 - 0
src/views/scan/ScanManage.vue

@@ -0,0 +1,30 @@
+<template>
+  <el-tabs v-model="activeTab" type="card" class="page-tab">
+    <el-tab-pane label="按科目统计" name="course">
+      <ScanCourseStats />
+    </el-tab-pane>
+    <el-tab-pane label="按考点统计" name="point">
+      <ScanPointStats />
+    </el-tab-pane>
+    <el-tab-pane label="签到表统计" name="signPaper">
+      <SignPaperStats />
+    </el-tab-pane>
+  </el-tabs>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { useRoute } from 'vue-router';
+
+  import ScanCourseStats from './ScanCourseStats.vue';
+  import ScanPointStats from './ScanPointStats.vue';
+  import SignPaperStats from './SignPaperStats.vue';
+
+  defineOptions({
+    name: 'ScanManage',
+  });
+
+  const route = useRoute();
+  // 从路由参数获取tab值,默认为course
+  const activeTab = ref((route.query.tab as string) || 'course');
+</script>

+ 90 - 0
src/views/scan/ScanPointStats.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="考点">
+        <el-input
+          v-model.trim="searchModel.point"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="exportData">导出</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column
+        property="totalStudents"
+        label="考生总数"
+        min-width="100"
+      />
+      <el-table-column
+        property="scannedSheets"
+        label="已扫张数"
+        min-width="100"
+      />
+      <el-table-column
+        property="scannedStudents"
+        label="已扫人数"
+        min-width="100"
+      />
+      <el-table-column
+        property="manualAbsent"
+        label="人工指定缺考"
+        min-width="100"
+      />
+      <el-table-column label="扫描进度" width="120">
+        <template #default="scope">
+          <el-progress
+            :percentage="scope.row.progress"
+            :color="getProgressColor(scope.row.progress)"
+          />
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      v-model:current-page="pagination.pageNumber"
+      v-model:page-size="pagination.pageSize"
+      :layout="pagination.layout"
+      :total="pagination.total"
+      @size-change="pageSizeChange"
+      @current-change="toPage"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive } from 'vue';
+  import { getScanPointList } from '@/api/scan';
+  import { ScanItem, ScanPointListFilter } from '@/api/types/scan';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'ScanPointStats',
+  });
+
+  const searchModel = reactive<ScanPointListFilter>({
+    point: '',
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<ScanItem>(getScanPointList, searchModel, false);
+
+  function getProgressColor(progress: number) {
+    if (progress < 30) return '#f56c6c';
+    if (progress < 70) return '#e6a23c';
+    return '#67c23a';
+  }
+
+  function exportData() {
+    // TODO: 实现导出功能
+  }
+</script>

+ 81 - 0
src/views/scan/SignPaperStats.vue

@@ -0,0 +1,81 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="签到表编号">
+        <el-input
+          v-model.trim="searchModel.signPaperNo"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-select
+          v-model="searchModel.status"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="请选择" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="exportData">总数 --</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column property="signPaperNo" label="签到表编号" width="150" />
+      <el-table-column property="imageCount" label="图片数量" width="100" />
+      <el-table-column label="操作" width="100" fixed="right">
+        <template #default="scope">
+          <el-button size="small" link @click="onView(scope.row)">
+            查看
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      v-model:current-page="pagination.pageNumber"
+      v-model:page-size="pagination.pageSize"
+      :layout="pagination.layout"
+      :total="pagination.total"
+      @size-change="pageSizeChange"
+      @current-change="toPage"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive } from 'vue';
+  import { getSignPaperStatList } from '@/api/scan';
+  import { SignPaperStatItem, SignPaperStatListFilter } from '@/api/types/scan';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'SignPaperStats',
+  });
+
+  const searchModel = reactive<SignPaperStatListFilter>({
+    signPaperNo: '',
+    status: '',
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<SignPaperStatItem>(getSignPaperStatList, searchModel, false);
+
+  function onView(row: SignPaperStatItem) {
+    // TODO: 实现查看功能
+    console.log('查看签到表:', row);
+  }
+
+  function exportData() {
+    // TODO: 实现导出功能
+  }
+</script>