Browse Source

feat: 分组管理

zhangjie 2 days ago
parent
commit
b69c19f4b7

+ 3 - 0
components.d.ts

@@ -29,11 +29,14 @@ declare module '@vue/runtime-core' {
     ElImageViewer: typeof import('element-plus/es')['ElImageViewer'];
     ElInput: typeof import('element-plus/es')['ElInput'];
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber'];
+    ElLink: typeof import('element-plus/es')['ElLink'];
     ElMenu: typeof import('element-plus/es')['ElMenu'];
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem'];
     ElOption: typeof import('element-plus/es')['ElOption'];
     ElPagination: typeof import('element-plus/es')['ElPagination'];
     ElProgress: typeof import('element-plus/es')['ElProgress'];
+    ElRadio: typeof import('element-plus/es')['ElRadio'];
+    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'];
     ElResult: typeof import('element-plus/es')['ElResult'];
     ElRow: typeof import('element-plus/es')['ElRow'];
     ElSelect: typeof import('element-plus/es')['ElSelect'];

+ 1 - 0
src/api/types/mark.ts

@@ -230,6 +230,7 @@ export type MarkMarkerListPageParam = PageParams<MarkMarkerListFilter>;
 // 分组管理
 // 分组管理列表:分组序号	大题号	大题名称	步骤分	包含选做题	评卷员人数	任务总数	完成总数	剩余总数	正在评卷	进度	评卷区设置	状态
 export interface MarkGroupItem {
+  id: number;
   // 分组序号
   groupNo: number;
   // 大题号

+ 76 - 4
src/router/routes/modules/base.ts

@@ -208,13 +208,85 @@ const BASE: AppRouteRecordRaw = {
       },
     },
     {
-      path: '/quality-monitor/score-curve',
-      name: 'ScoreCurve',
-      component: () => import('@/views/mark/ScoreCurve.vue'),
+      path: '/mark-manage',
+      name: 'MarkManage',
+      component: () => import('@/views/mark/MarkManage.vue'),
       meta: {
-        title: '给分曲线',
+        title: '评卷管理',
         requiresAuth: true,
       },
+      children: [
+        // 分组管理
+        {
+          path: '/mark-manage/group',
+          name: 'GroupManage',
+          component: () => import('@/views/mark/GroupManage.vue'),
+          meta: {
+            title: '分组管理',
+            requiresAuth: true,
+          },
+        },
+        // 评卷员管理
+        {
+          path: '/mark-manage/marker',
+          name: 'MarkerManage',
+          component: () => import('@/views/mark/MarkerManage.vue'),
+          meta: {
+            title: '评卷员管理',
+            requiresAuth: true,
+          },
+        },
+        // 试评管理
+        {
+          path: '/mark-manage/trial',
+          name: 'TrialManage',
+          component: () => import('@/views/mark/TrialManage.vue'),
+          meta: {
+            title: '试评管理',
+            requiresAuth: true,
+          },
+        },
+        // 任务管理
+        {
+          path: '/mark-manage/task',
+          name: 'TaskManage',
+          component: () => import('@/views/mark/TaskManage.vue'),
+          meta: {
+            title: '任务管理',
+            requiresAuth: true,
+          },
+        },
+        // 仲裁管理
+        {
+          path: '/mark-manage/arbitration',
+          name: 'ArbitrationManage',
+          component: () => import('@/views/mark/ArbitrationManage.vue'),
+          meta: {
+            title: '仲裁管理',
+            requiresAuth: true,
+          },
+        },
+        // 质量监控
+        {
+          path: '/quality-monitor',
+          name: 'QualityMonitor',
+          component: () => import('@/views/mark/QualityMonitor.vue'),
+          meta: {
+            title: '质量监控',
+            requiresAuth: true,
+          },
+        },
+        // 给分曲线
+        {
+          path: '/quality-monitor/score-curve',
+          name: 'ScoreCurve',
+          component: () => import('@/views/mark/ScoreCurve.vue'),
+          meta: {
+            title: '给分曲线',
+            requiresAuth: true,
+          },
+        },
+      ],
     },
   ],
 };

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

@@ -105,7 +105,7 @@ export const adminMenus = [
   {
     id: 9,
     name: '科目评卷管理',
-    url: 'SubjectMarkManage',
+    url: 'MarkManage',
     type: 'MENU',
     parentId: 7,
     sequence: 2,

+ 195 - 0
src/views/mark/GroupManage.vue

@@ -0,0 +1,195 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subject"></select-subject>
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="onAdd">新增</el-button>
+          <el-button @click="onDataCheck">数量校对</el-button>
+          <el-button @click="onClose">关闭</el-button>
+          <el-button @click="onSetTrialCount">设置试评数量</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+
+  <div class="part-box">
+    <el-table
+      class="page-table"
+      :data="dataList"
+      :loading="loading"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" />
+      <el-table-column property="groupNo" label="分组序号" width="100" />
+      <el-table-column property="questionNo" label="大题号" width="100" />
+      <el-table-column
+        property="questionName"
+        label="大题名称"
+        min-width="150"
+        show-overflow-tooltip
+      />
+      <el-table-column property="stepScore" label="步骤分" width="100" />
+      <el-table-column label="包含选做题" width="120">
+        <template #default="scope">
+          <el-tag
+            :type="scope.row.hasOptional ? 'success' : 'info'"
+            size="small"
+          >
+            {{ scope.row.hasOptional ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column property="markerCount" label="评卷员人数" width="120" />
+      <el-table-column property="totalTasks" label="任务总数" width="100" />
+      <el-table-column property="completedTasks" label="完成总数" width="100" />
+      <el-table-column property="remainingTasks" label="剩余总数" width="100" />
+      <el-table-column label="正在评卷" width="100">
+        <template #default="scope">
+          <el-tag :type="scope.row.isMarking ? 'success' : 'info'" size="small">
+            {{ scope.row.isMarking ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="进度" width="120">
+        <template #default="scope">
+          <el-progress
+            :percentage="scope.row.progress"
+            :color="scope.row.progress === 100 ? '#67c23a' : '#409eff'"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="markingAreaSetting"
+        label="评卷区设置"
+        min-width="150"
+        show-overflow-tooltip
+      />
+      <el-table-column label="操作" width="120" fixed="right">
+        <template #default="scope">
+          <el-button
+            type="primary"
+            size="small"
+            link
+            @click="onEdit(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>
+
+  <SetTrialCountDialog
+    ref="setTrialCountDialogRef"
+    :subject-id="searchModel.subject"
+  />
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref, computed } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import {
+    getMarkGroupList,
+    markGroupDataCheck,
+    markGroupClose,
+  } from '@/api/mark';
+  import { MarkGroupItem, MarkGroupListFilter } from '@/api/types/mark';
+  import useTable from '@/hooks/table';
+  import { modalConfirm } from '@/utils/ui';
+
+  import SetTrialCountDialog from './components/SetTrialCountDialog.vue';
+
+  defineOptions({
+    name: 'GroupManage',
+  });
+
+  const searchModel = reactive<MarkGroupListFilter>({
+    subject: null,
+  });
+
+  const setTrialCountDialogRef =
+    ref<InstanceType<typeof SetTrialCountDialog>>();
+
+  const {
+    dataList,
+    pagination,
+    loading,
+    selectedRows,
+    toPage,
+    getList,
+    pageSizeChange,
+    handleSelectionChange,
+  } = useTable<MarkGroupItem>(getMarkGroupList, searchModel, false);
+
+  // 计算选中的分组ID列表
+  const selectedGroupIds = computed(() => {
+    return selectedRows.value.map((row) => row.id);
+  });
+
+  // 新增分组
+  function onAdd() {
+    ElMessage.info('新增分组功能待实现');
+    // TODO: 实现新增分组的逻辑
+  }
+
+  // 修改分组
+  function onEdit(row: MarkGroupItem) {
+    ElMessage.info(`修改分组:${row.groupNo}`);
+    // TODO: 实现修改分组的逻辑,功能空着留着后面做
+  }
+
+  // 数量校对
+  async function onDataCheck() {
+    if (!searchModel.subject) {
+      ElMessage.warning('请选择科目');
+      return;
+    }
+
+    try {
+      await markGroupDataCheck(searchModel.subject);
+      ElMessage.success('数量校对成功');
+      getList();
+    } catch (error) {
+      console.error('数量校对失败:', error);
+    }
+  }
+
+  // 关闭分组
+  async function onClose() {
+    if (!selectedRows.value.length) {
+      ElMessage.warning('请选择分组');
+      return;
+    }
+
+    const confirm = await modalConfirm(
+      `确认关闭所选的 ${selectedRows.value.length} 个分组的评卷吗?`,
+      '提示 '
+    ).catch(() => false);
+    if (!confirm) return;
+
+    try {
+      await markGroupClose(selectedGroupIds.value);
+      ElMessage.success('批量关闭成功');
+      getList();
+    } catch (error) {
+      console.error('批量关闭失败:', error);
+    }
+  }
+
+  // 设置试评数量
+  function onSetTrialCount() {
+    setTrialCountDialogRef.value?.open();
+  }
+</script>

+ 117 - 0
src/views/mark/MarkManage.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="mark-manage-container">
+    <!-- 顶部标签导航 -->
+    <el-tabs v-model="activeMenu" class="mark-tabs" @tab-click="handleTabClick">
+      <el-tab-pane label="分组管理" name="GroupManage">
+        <template #label>
+          <span>分组管理</span>
+        </template>
+      </el-tab-pane>
+      <el-tab-pane label="评卷员管理" name="MarkerManage">
+        <template #label>
+          <span>评卷员管理</span>
+        </template>
+      </el-tab-pane>
+      <el-tab-pane label="试评管理" name="TrialManage">
+        <template #label>
+          <span>试评管理</span>
+        </template>
+      </el-tab-pane>
+      <el-tab-pane label="任务管理" name="TaskManage">
+        <template #label>
+          <span>任务管理</span>
+        </template>
+      </el-tab-pane>
+      <el-tab-pane label="仲裁管理" name="ArbitrationManage">
+        <template #label>
+          <span>仲裁管理</span>
+        </template>
+      </el-tab-pane>
+      <el-tab-pane label="质量监控" name="QualityMonitor">
+        <template #label>
+          <span>质量监控</span>
+        </template>
+      </el-tab-pane>
+    </el-tabs>
+
+    <!-- 子页面内容区域 -->
+    <div class="content-container">
+      <router-view />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch, onMounted } from 'vue';
+  import { useRoute, useRouter } from 'vue-router';
+
+  defineOptions({
+    name: 'MarkManage',
+  });
+
+  const route = useRoute();
+  const router = useRouter();
+
+  // 当前激活的菜单项
+  const activeMenu = ref<string>('');
+
+  // 标签点击处理
+  function handleTabClick(tab: any) {
+    const tabName = tab.paneName || tab.props.name;
+    if (tabName !== route.name) {
+      router.push({ name: tabName });
+    }
+  }
+
+  // 监听路由变化,更新激活菜单
+  watch(
+    () => route.name,
+    (newName) => {
+      activeMenu.value = newName as string;
+    },
+    { immediate: true }
+  );
+
+  // 组件挂载时初始化
+  onMounted(() => {
+    // 如果当前路径是父路由,默认跳转到分组管理
+    if (route.name === 'MarkManage') {
+      router.replace({ name: 'GroupManage' });
+    } else {
+      activeMenu.value = route.name as string;
+    }
+  });
+</script>
+
+<style scoped>
+  .mark-manage-container {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .mark-tabs {
+    border-bottom: 1px solid var(--el-border-color-light);
+  }
+
+  .mark-tabs :deep(.el-tabs__item) {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .mark-tabs :deep(.el-tabs__content) {
+    display: none;
+  }
+
+  .content-container {
+    flex: 1;
+    overflow: auto;
+    padding: 0;
+  }
+
+  /* 移除子页面的顶部间距 */
+  .content-container :deep(.part-box:first-child) {
+    margin-top: 0;
+  }
+</style>

+ 2 - 0
src/views/mark/MarkProgress.vue

@@ -111,6 +111,8 @@
 <script setup lang="ts">
   import { reactive, ref, computed, onMounted } from 'vue';
   import { ElMessage } from 'element-plus';
+  import { ArrowDown } from '@element-plus/icons-vue';
+
   import {
     getMarkStatInfo,
     getMarkStatList,

+ 2 - 2
src/views/mark/TaskManage.vue

@@ -99,8 +99,8 @@
       <el-form-item>
         <el-space wrap>
           <el-button type="primary" @click="toPage(1)">查询</el-button>
-          <el-button @click="onBatchReject">批量打回</el-button>
-          <el-button @click="onExport">导出</el-button>
+          <!--TODO: 待复核数量 -->
+          <el-button @click="onExport">待复核:0</el-button>
         </el-space>
       </el-form-item>
     </el-form>

+ 120 - 0
src/views/mark/components/SetTrialCountDialog.vue

@@ -0,0 +1,120 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="设置试评数量"
+    width="500px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    top="10vh"
+    append-to-body
+    @close="handleClose"
+    @open="modalBeforeOpen"
+  >
+    <el-form
+      ref="formRef"
+      :model="formModel"
+      :rules="rules"
+      label-width="100px"
+    >
+      <el-form-item label="试评数量" prop="count">
+        <el-input-number
+          v-model="formModel.count"
+          placeholder="请输入试评数量"
+          :min="0"
+          :max="9999"
+          :step="1"
+          :controls="true"
+          step-strictly
+          style="width: 100%"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="close">取消</el-button>
+      <el-button type="primary" :loading="loading" @click="confirm">
+        确定
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive } from 'vue';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import { ElMessage } from 'element-plus';
+  import { markGroupSetTrialCount } from '@/api/mark';
+  import useModal from '@/hooks/modal';
+  import useLoading from '@/hooks/loading';
+  import { objAssign } from '@/utils/utils';
+
+  defineOptions({
+    name: 'SetTrialCountDialog',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  interface Props {
+    subjectId?: number; // 科目ID
+  }
+
+  const props = defineProps<Props>();
+  const emit = defineEmits(['modified']);
+
+  const formRef = ref<FormInstance>();
+
+  interface FormModel {
+    subjectId: number;
+    count: number;
+  }
+
+  const initialFormState: FormModel = {
+    subjectId: 0,
+    count: 0,
+  };
+
+  const formModel = reactive<FormModel>({ ...initialFormState });
+
+  const rules: FormRules<keyof FormModel> = {
+    count: [{ required: true, message: '请输入试评数量', trigger: 'change' }],
+  };
+
+  const handleClose = () => {
+    formRef.value?.resetFields();
+    Object.assign(formModel, initialFormState);
+  };
+
+  /* confirm */
+  const { loading, setLoading } = useLoading();
+  async function confirm() {
+    if (!formRef.value) return;
+
+    const valid = await formRef.value?.validate().catch(() => false);
+    if (!valid) return;
+
+    if (!formModel.subjectId) {
+      ElMessage.warning('请选择科目');
+      return;
+    }
+
+    setLoading(true);
+    const datas = objAssign(formModel, {});
+    let res = true;
+    await markGroupSetTrialCount(datas).catch(() => {
+      res = false;
+    });
+    setLoading(false);
+    if (!res) return;
+    ElMessage.success('设置试评数量成功!');
+    emit('modified');
+    close();
+  }
+
+  /* init modal */
+  function modalBeforeOpen() {
+    if (props.subjectId) {
+      formModel.subjectId = props.subjectId;
+    }
+  }
+</script>

+ 1 - 0
src/views/subject/SubjectManage.vue

@@ -227,6 +227,7 @@
 <script setup lang="ts">
   import { reactive, ref, onMounted, computed } from 'vue';
   import { useRouter } from 'vue-router';
+  import { ArrowDown } from '@element-plus/icons-vue';
   import {
     getSubjectList,
     getSubjectTotalScoreStatList,