ソースを参照

feat: 任务管理

zhangjie 2 日 前
コミット
1550db8391

+ 3 - 2
src/api/mark.ts

@@ -19,6 +19,7 @@ import {
   MarkStatInfo,
   MarkStatInfo,
   MarkStatListPageParam,
   MarkStatListPageParam,
   MarkStatListPageRes,
   MarkStatListPageRes,
+  MarkRejectTaskParam,
 } from './types/mark';
 } from './types/mark';
 
 
 // 质量监控
 // 质量监控
@@ -58,8 +59,8 @@ export function getMarkTaskList(
 }
 }
 
 
 // 任务打回
 // 任务打回
-export function markTaskReject(id: number): Promise<boolean> {
-  return axios.post('/api/mark/task/reject', {}, { params: { id } });
+export function markTaskReject(params: MarkRejectTaskParam): Promise<boolean> {
+  return axios.post('/api/mark/task/reject', {}, { params });
 }
 }
 
 
 // 试评管理
 // 试评管理

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

@@ -142,10 +142,18 @@ export interface MarkTaskListFilter {
   totalStartScore?: number;
   totalStartScore?: number;
   totalEndScore?: number;
   totalEndScore?: number;
   // 小题得分
   // 小题得分
-  questionScore?: number;
+  smallQuestionScore?: number;
 }
 }
 export type MarkTaskListPageParam = PageParams<MarkTaskListFilter>;
 export type MarkTaskListPageParam = PageParams<MarkTaskListFilter>;
 
 
+export interface MarkRejectTaskParam {
+  id: number;
+  // 打回类型
+  rejectType: string;
+  // 打回原因
+  rejectReason: string;
+}
+
 // 试评管理
 // 试评管理
 // 试评管理列表:科目代码	分组序号	准考证号	密号	评卷员	评卷总分	给分明细	评卷时间
 // 试评管理列表:科目代码	分组序号	准考证号	密号	评卷员	评卷总分	给分明细	评卷时间
 export interface MarkTrialItem {
 export interface MarkTrialItem {

+ 1 - 1
src/router/routes/modules/base.ts

@@ -210,7 +210,7 @@ const BASE: AppRouteRecordRaw = {
     {
     {
       path: '/quality-monitor/score-curve',
       path: '/quality-monitor/score-curve',
       name: 'ScoreCurve',
       name: 'ScoreCurve',
-      component: () => import('@/views/mark/quality-monitor/ScoreCurve.vue'),
+      component: () => import('@/views/mark/ScoreCurve.vue'),
       meta: {
       meta: {
         title: '给分曲线',
         title: '给分曲线',
         requiresAuth: true,
         requiresAuth: true,

+ 0 - 0
src/views/mark/quality-monitor/QualityMonitor.vue → src/views/mark/QualityMonitor.vue


+ 0 - 0
src/views/mark/quality-monitor/ScoreCurve.vue → src/views/mark/ScoreCurve.vue


+ 273 - 0
src/views/mark/TaskManage.vue

@@ -0,0 +1,273 @@
+<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 label="分组">
+        <el-input
+          v-model.trim="searchModel.group"
+          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="pending" />
+          <el-option label="已评卷" value="completed" />
+          <el-option label="已打回" value="rejected" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="评卷员">
+        <el-input
+          v-model.number="searchModel.markerId"
+          placeholder="请输入评卷员ID"
+          clearable
+          style="width: 150px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item label="准考证号">
+        <el-input
+          v-model.trim="searchModel.examCardNo"
+          placeholder="请输入准考证号"
+          clearable
+          style="width: 150px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item label="密号">
+        <el-input
+          v-model.trim="searchModel.secretNo"
+          placeholder="请输入密号"
+          clearable
+          style="width: 120px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item label="总分">
+        <el-space>
+          <span>从</span>
+          <el-input-number
+            v-model="searchModel.totalStartScore"
+            placeholder="低分"
+            :min="0"
+            :max="999"
+            :step="0.1"
+            :precision="1"
+            :controls="false"
+            step-strictly
+            style="width: 80px"
+          />
+          <span>到</span>
+          <el-input-number
+            v-model="searchModel.totalEndScore"
+            placeholder="高分"
+            :min="0"
+            :max="999"
+            :step="0.1"
+            :precision="1"
+            :controls="false"
+            step-strictly
+            style="width: 80px"
+          />
+        </el-space>
+      </el-form-item>
+      <el-form-item label="小题得分">
+        <el-input-number
+          v-model="searchModel.smallQuestionScore"
+          placeholder=""
+          :min="0"
+          :max="999"
+          :step="0.1"
+          :precision="1"
+          :controls="false"
+          step-strictly
+          style="width: 120px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-checkbox v-model="searchModel.optional"> 选做题 </el-checkbox>
+      </el-form-item>
+      <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>
+        </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="subjectCode" label="科目代码" width="120" />
+      <el-table-column property="groupNo" label="分组序号" width="100" />
+      <el-table-column property="examCardNo" label="准考证号" width="150" />
+      <el-table-column property="secretNo" label="密号" width="120" />
+      <el-table-column label="状态" width="100">
+        <template #default="scope">
+          <el-tag :type="getStatusType(scope.row.status)" size="small">
+            {{ getStatusText(scope.row.status) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="returnReason"
+        label="打回原因"
+        min-width="150"
+        show-overflow-tooltip
+      >
+        <template #default="scope">
+          {{ scope.row.returnReason || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column property="marker" label="评卷员" width="120" />
+      <el-table-column property="totalScore" label="评卷总分" width="100" />
+      <el-table-column
+        property="giveScoreDetail"
+        label="给分明细"
+        min-width="200"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        property="markingTime"
+        label="评卷时间"
+        width="180"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        property="reviewer"
+        label="复核人"
+        width="120"
+        show-overflow-tooltip
+      >
+        <template #default="scope">
+          {{ scope.row.reviewer || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="reviewTime"
+        label="复核时间"
+        width="180"
+        show-overflow-tooltip
+      >
+        <template #default="scope">
+          {{ scope.row.reviewTime || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="120" fixed="right">
+        <template #default="scope">
+          <el-button
+            type="primary"
+            size="small"
+            link
+            @click="onView(scope.row)"
+          >
+            查看
+          </el-button>
+          <el-button
+            v-if="scope.row.status === 'completed'"
+            type="danger"
+            size="small"
+            link
+            @click="onReject(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>
+
+  <RejectTaskDialog
+    ref="rejectTaskDialogRef"
+    :task-id="curRow.id"
+    @modified="getList"
+  />
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import { getMarkTaskList } from '@/api/mark';
+  import { MarkTaskItem, MarkTaskListFilter } from '@/api/types/mark';
+  import useTable from '@/hooks/table';
+
+  import RejectTaskDialog from './components/RejectTaskDialog.vue';
+
+  defineOptions({
+    name: 'TaskManage',
+  });
+
+  const searchModel = reactive<MarkTaskListFilter>({
+    subject: null,
+    group: '',
+    status: '',
+    markerId: undefined,
+    optional: undefined,
+    examCardNo: '',
+    secretNo: '',
+    totalStartScore: undefined,
+    totalEndScore: undefined,
+    smallQuestionScore: undefined,
+  });
+
+  const curRow = ref<MarkTaskItem>({});
+  const rejectTaskDialogRef = ref<InstanceType<typeof RejectTaskDialog>>();
+
+  const { dataList, pagination, loading, toPage, getList, pageSizeChange } =
+    useTable<MarkTaskItem>(getMarkTaskList, searchModel, false);
+
+  // 获取状态类型
+  function getStatusType(status: string) {
+    const statusMap: Record<string, string> = {
+      pending: 'warning',
+      completed: 'success',
+      rejected: 'danger',
+    };
+    return statusMap[status] || 'info';
+  }
+
+  // 获取状态文本
+  function getStatusText(status: string) {
+    const statusMap: Record<string, string> = {
+      pending: '待评卷',
+      completed: '已评卷',
+      rejected: '已打回',
+    };
+    return statusMap[status] || status;
+  }
+
+  // 查看详情
+  function onView(row: MarkTaskItem) {
+    ElMessage.info(`查看任务:${row.examCardNo}`);
+    // TODO: 实现查看任务详情的逻辑
+  }
+
+  // 打回任务
+  async function onReject(row: MarkTaskItem) {
+    curRow.value = row;
+    rejectTaskDialogRef.value?.show();
+  }
+
+  function onExport() {
+    // TODO: 导出任务
+    ElMessage.info('导出任务');
+  }
+</script>

+ 151 - 0
src/views/mark/components/RejectTaskDialog.vue

@@ -0,0 +1,151 @@
+<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="rejectType">
+        <el-select
+          v-model="formModel.rejectType"
+          placeholder="请选择打回原因"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="item in rejectTypeList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.name"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="详情描述">
+        <el-input
+          v-model="formModel.rejectReason"
+          type="textarea"
+          :rows="4"
+          placeholder="请输入详情描述"
+          maxlength="500"
+          show-word-limit
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="close">取消</el-button>
+        <el-button type="primary" :loading="loading" @click="confirm">
+          打回
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, onMounted } from 'vue';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import { ElMessage } from 'element-plus';
+  import type { MarkRejectTaskParam } from '@/api/types/mark';
+  import type { RejectTypeItem } from '@/api/types/reject';
+  import { markTaskReject } from '@/api/mark';
+  import { getRejectTypeList } from '@/api/reject';
+  import useModal from '@/hooks/modal';
+  import useLoading from '@/hooks/loading';
+  import { objAssign } from '@/utils/utils';
+  import { useAppStore } from '@/store';
+
+  defineOptions({
+    name: 'RejectTaskDialog',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  interface Props {
+    taskId?: number; // 任务ID
+  }
+
+  const props = defineProps<Props>();
+  const emit = defineEmits(['modified']);
+
+  const appStore = useAppStore();
+
+  const formRef = ref<FormInstance>();
+  const rejectTypeList = ref<RejectTypeItem[]>([]);
+
+  const initialFormState: MarkRejectTaskParam = {
+    id: 0,
+    rejectType: '',
+    rejectReason: '',
+  };
+
+  const formModel = reactive<MarkRejectTaskParam>({ ...initialFormState });
+
+  const rules: FormRules<keyof MarkRejectTaskParam> = {
+    rejectType: [
+      { 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;
+
+    setLoading(true);
+    const datas = objAssign(formModel, {});
+    let res = true;
+    await markTaskReject(datas).catch(() => {
+      res = false;
+    });
+    setLoading(false);
+    if (!res) return;
+    ElMessage.success('打回成功!');
+    emit('modified');
+    close();
+  }
+
+  /* init modal */
+  function modalBeforeOpen() {
+    if (props.taskId) {
+      formModel.id = props.taskId;
+    }
+  }
+
+  /* 获取打回类型列表 */
+  async function loadRejectTypeList() {
+    if (!appStore.curExam.id) return;
+    try {
+      const res = await getRejectTypeList(appStore.curExam.id);
+      rejectTypeList.value = res || [];
+    } catch (error) {
+      console.error('获取打回类型列表失败:', error);
+      rejectTypeList.value = [];
+    }
+  }
+
+  onMounted(() => {
+    // 获取打回类型列表
+    loadRejectTypeList();
+  });
+</script>