Bläddra i källkod

feat: 分组编辑

zhangjie 1 dag sedan
förälder
incheckning
a35f10a4ad

+ 5 - 0
src/api/mark.ts

@@ -135,6 +135,11 @@ export function markGroupEnd(id: number): Promise<boolean> {
   return axios.post('/api/mark/group/end', {}, { params: { id } });
   return axios.post('/api/mark/group/end', {}, { params: { id } });
 }
 }
 
 
+// 获取分组信息
+export function getMarkGroupInfo(id: number): Promise<MarkGroupUpdateParams> {
+  return axios.post('/api/mark/group/info', {}, { params: { id } });
+}
+
 // 修改分组
 // 修改分组
 export function markGroupUpdate(
 export function markGroupUpdate(
   params: MarkGroupUpdateParams
   params: MarkGroupUpdateParams

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

@@ -1,3 +1,8 @@
+import {
+  CombineScoreStrategy,
+  ArbitrationType,
+  ThreeEvaluationRule,
+} from '@/constants/enumerate';
 import { PageResult, PageParams, CoverArea } from './common';
 import { PageResult, PageParams, CoverArea } from './common';
 
 
 // 质量监控
 // 质量监控
@@ -268,6 +273,7 @@ export type MarkGroupListPageParam = PageParams<MarkGroupListFilter>;
 export interface MarkGroupUpdateParams {
 export interface MarkGroupUpdateParams {
   // id
   // id
   id?: number;
   id?: number;
+  subjectId: number;
   // 分组序号
   // 分组序号
   groupNo: number;
   groupNo: number;
   // 名称
   // 名称
@@ -275,16 +281,28 @@ export interface MarkGroupUpdateParams {
   // 题目
   // 题目
   questions: Array<{
   questions: Array<{
     // 大题号
     // 大题号
-    questionNo: string;
+    bigQuestionNo: string;
     // 小题号
     // 小题号
     smallQuestionNo: string;
     smallQuestionNo: string;
     // 间隔分
     // 间隔分
     intervalScore: number;
     intervalScore: number;
+    // 仲裁阀值
+    arbitrationThreshold?: number;
   }>;
   }>;
   // 评卷图
   // 评卷图
   markingArea?: CoverArea[];
   markingArea?: CoverArea[];
   // 双评
   // 双评
   doubleMarking: boolean;
   doubleMarking: boolean;
+  // 仲裁方式
+  arbitrationType?: ArbitrationType;
+  // 双评比例
+  doubleMarkingRatio?: number;
+  // 仲裁阀值
+  arbitrationThreshold?: number;
+  // 合分策略
+  combinationStrategy?: CombineScoreStrategy;
+  // 三评规则
+  threeMarkingRule?: ThreeEvaluationRule;
 }
 }
 
 
 // 评卷进度
 // 评卷进度

+ 4 - 0
src/api/types/subject.ts

@@ -120,10 +120,14 @@ export interface OptionalQuestionItem {
   bigQuestionNo: number;
   bigQuestionNo: number;
   // 小题号
   // 小题号
   smallQuestionNo: number;
   smallQuestionNo: number;
+  // 大题昵称
+  bigQuestionNickname: string;
   // 满分
   // 满分
   fullScore: number;
   fullScore: number;
   // 间隔分
   // 间隔分
   intervalScore: number;
   intervalScore: number;
+  // 选做题
+  optional: boolean;
   // 选做题分组
   // 选做题分组
   optionalQuestionGroup: string;
   optionalQuestionGroup: string;
   // 选做题区
   // 选做题区

+ 22 - 0
src/constants/enumerate.ts

@@ -85,3 +85,25 @@ export const OPTIONAL_SCORE_RULE = {
   MIN_SCORE_NO_ZERO: '最低分(去掉0分)',
   MIN_SCORE_NO_ZERO: '最低分(去掉0分)',
 };
 };
 export type OptionalScoreRule = keyof typeof OPTIONAL_SCORE_RULE;
 export type OptionalScoreRule = keyof typeof OPTIONAL_SCORE_RULE;
+
+// 仲裁方式:按小题、按分组
+export const ARBITRATION_TYPE = {
+  QUESTION: '按小题仲裁',
+  GROUP: '按分组仲裁',
+};
+export type ArbitrationType = keyof typeof ARBITRATION_TYPE;
+
+// 合分策略: 平均分、最高分、最低分
+export const COMBINE_SCORE_STRATEGY = {
+  AVG_SCORE: '平均分',
+  MAX_SCORE: '最高分',
+  MIN_SCORE: '最低分',
+};
+export type CombineScoreStrategy = keyof typeof COMBINE_SCORE_STRATEGY;
+
+// 三评规则:关闭、低差值高均分
+export const THREE_EVALUATION_RULE = {
+  CLOSE: '关闭',
+  LOW_DIFF_HIGH_AVG: '低差值高均分',
+};
+export type ThreeEvaluationRule = keyof typeof THREE_EVALUATION_RULE;

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

@@ -226,6 +226,16 @@ const BASE: AppRouteRecordRaw = {
             requiresAuth: true,
             requiresAuth: true,
           },
           },
         },
         },
+        {
+          // 编辑分组
+          path: '/mark-manage/group/:subjectId/edit/:groupId?',
+          name: 'GroupEdit',
+          component: () => import('@/views/mark/group-edit/GroupEdit.vue'),
+          meta: {
+            title: '编辑分组',
+            requiresAuth: true,
+          },
+        },
         // 评卷员管理
         // 评卷员管理
         {
         {
           path: '/mark-manage/marker',
           path: '/mark-manage/marker',

+ 185 - 0
src/views/mark/group-edit/GroupEdit.vue

@@ -0,0 +1,185 @@
+<template>
+  <el-steps :active="currentStep" finish-status="success">
+    <el-step title="勾选试题分组" />
+    <el-step title="分组参数设置" />
+  </el-steps>
+
+  <div style="margin-top: 20px">
+    <!-- 第一步:选择试题 -->
+    <div v-if="currentStep === 0">
+      <GroupQuestionSelect
+        ref="questionSelectRef"
+        v-model="ruleFormData.questions"
+        :subject-id="props.rowData?.subjectId"
+      />
+    </div>
+    <!-- 第二步:设置规则 -->
+    <div v-if="currentStep === 1">
+      <GroupRuleForm
+        ref="ruleFormRef"
+        v-model="ruleFormData"
+        :edit-type="editType"
+      />
+    </div>
+  </div>
+
+  <div class="dialog-footer">
+    <el-button @click="cancel">取消</el-button>
+    <el-button v-if="currentStep > 0 && !isEdit" @click="prevStep"
+      >上一步</el-button
+    >
+    <el-button v-if="currentStep < 1" type="primary" @click="nextStep">
+      下一步
+    </el-button>
+    <el-button
+      v-if="currentStep === 1 && isReset"
+      type="danger"
+      @click="onDelete"
+    >
+      删除
+    </el-button>
+    <el-button
+      v-if="currentStep === 1 && isEdit"
+      type="danger"
+      @click="onReset"
+    >
+      重置修改
+    </el-button>
+    <el-button
+      v-if="currentStep === 1"
+      type="primary"
+      :loading="loading"
+      @click="confirm"
+    >
+      保存
+    </el-button>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed, reactive, onMounted } from 'vue';
+  import { useRoute, useRouter } from 'vue-router';
+  import { ElMessage } from 'element-plus';
+  import { getMarkGroupInfo, markGroupUpdate } from '@/api/mark';
+  import type { MarkGroupUpdateParams } from '@/api/types/mark';
+  import useLoading from '@/hooks/loading';
+
+  import { objModifyAssign } from '@/utils/utils';
+
+  import GroupRuleForm from './GroupRuleForm.vue';
+  import GroupQuestionSelect from './GroupQuestionSelect.vue';
+
+  defineOptions({
+    name: 'GroupEdit',
+  });
+
+  // 编辑类型:新增、编辑、重置
+  type EditType = 'add' | 'edit' | 'reset';
+
+  const route = useRoute();
+  const router = useRouter();
+
+  const editType = ref<EditType>('add');
+  const isEdit = computed(() => editType.value === 'edit');
+  const isReset = computed(() => editType.value === 'reset');
+  // const isAdd = computed(() => editType.value === 'add');
+
+  const currentStep = ref(0);
+  const ruleFormRef = ref();
+  const questionSelectRef = ref();
+
+  // 规则表单数据
+  const getInitialRuleFormData = (): MarkGroupUpdateParams => {
+    return {
+      id: route.params?.groupId,
+      subjectId: route.params.subjectId,
+      groupNo: 0,
+      name: '',
+      markingArea: [],
+      questions: [],
+      doubleMarking: false,
+      arbitrationType: 'GROUP',
+      doubleMarkingRatio: undefined,
+      arbitrationThreshold: undefined,
+      combinationStrategy: 'AVG_SCORE',
+      threeMarkingRule: 'CLOSE',
+    };
+  };
+  const ruleFormData = reactive(getInitialRuleFormData());
+
+  // 选中的试题
+  const selectedQuestions = ref<MarkGroupUpdateParams['questions']>([]);
+
+  // 下一步
+  const nextStep = async () => {
+    if (currentStep.value === 0) {
+      // 验证第一步表单
+
+      const valid = await questionSelectRef.value
+        ?.validate()
+        .catch(() => false);
+      if (!valid) return;
+    }
+    currentStep.value++;
+  };
+
+  // 上一步
+  const prevStep = () => {
+    currentStep.value--;
+  };
+
+  const onReset = () => {
+    editType.value = 'reset';
+    currentStep.value = 0;
+  };
+
+  const onDelete = async () => {
+    // TODO: 删除分组
+  };
+
+  // 提交
+  const { loading, setLoading } = useLoading();
+  const confirm = async () => {
+    // 验证第二步
+    const valid = await ruleFormRef.value?.validate().catch(() => false);
+    if (!valid) return;
+
+    setLoading(true);
+    const params: MarkGroupUpdateParams = {
+      ...ruleFormData,
+      questions: selectedQuestions.value,
+    };
+
+    let res = true;
+    await markGroupUpdate(params).catch(() => {
+      res = false;
+    });
+    setLoading(false);
+    if (!res) return;
+
+    ElMessage.success(`保存成功!`);
+  };
+
+  /* init modal */
+  async function initData() {
+    if (route.params?.groupId) {
+      // 编辑模式
+      const res = await getMarkGroupInfo(route.params.groupId);
+      objModifyAssign(ruleFormData, res);
+      editType.value = 'edit';
+      currentStep.value = 1;
+    } else {
+      editType.value = 'add';
+      currentStep.value = 0;
+    }
+    currentStep.value = 0;
+  }
+
+  function cancel() {
+    router.go(-1);
+  }
+
+  onMounted(() => {
+    initData();
+  });
+</script>

+ 132 - 0
src/views/mark/group-edit/GroupQuestionSelect.vue

@@ -0,0 +1,132 @@
+<template>
+  <el-table
+    ref="tableRef"
+    v-loading="loading"
+    :data="dataList"
+    class="question-table"
+    @selection-change="handleSelectionChange"
+  >
+    <el-table-column
+      type="selection"
+      width="55"
+      :selectable="
+        (row) =>
+          props.disabledIds.includes(
+            `${row.bigQuestionNo}-${row.smallQuestionNo}`
+          )
+      "
+    />
+    <el-table-column prop="bigQuestionNo" label="大题号" width="80" />
+    <el-table-column prop="smallQuestionNo" label="小题号" width="80" />
+    <el-table-column prop="bigQuestionName" label="名称" min-width="120" />
+    <el-table-column prop="bigQuestionNickname" label="昵称" min-width="120" />
+    <el-table-column prop="fullScore" label="分数" width="80" />
+    <el-table-column prop="optional" label="选做题" width="80" />
+    <el-table-column
+      prop="optionalQuestionGroup"
+      label="选做题分组"
+      width="100"
+    />
+    <el-table-column prop="optionalQuestionArea" label="选做题区" width="100" />
+  </el-table>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted, nextTick } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import { getOptionalQuestionList } from '@/api/subject';
+  import type { OptionalQuestionItem } from '@/api/types/subject';
+  import type { MarkGroupUpdateParams } from '@/api/types/mark';
+
+  import useLoading from '@/hooks/loading';
+
+  defineOptions({
+    name: 'GroupQuestionSelect',
+  });
+
+  interface Props {
+    modelValue: MarkGroupUpdateParams['questions'];
+    subjectId: number;
+    disabledIds: string[];
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    modelValue: [],
+    subjectId: 0,
+    disabledIds: () => [],
+  });
+
+  const emit = defineEmits<{
+    'update:modelValue': [value: MarkGroupUpdateParams['questions']];
+  }>();
+
+  const tableRef = ref();
+  const dataList = ref<OptionalQuestionItem[]>([]);
+  const { loading, setLoading } = useLoading();
+
+  // 设置已选中的行
+  const setSelectedRows = () => {
+    if (!tableRef.value || !props.modelValue.length) return;
+    const selectedIds = props.modelValue.map(
+      (item) => `${item.bigQuestionNo}-${item.smallQuestionNo}`
+    );
+
+    dataList.value.forEach((row) => {
+      if (selectedIds.includes(`${row.bigQuestionNo}-${row.smallQuestionNo}`)) {
+        tableRef.value.toggleRowSelection(row, true);
+      }
+    });
+  };
+
+  // 获取试题列表
+  const getList = async () => {
+    if (!props.subjectId) return;
+
+    setLoading(true);
+    try {
+      // TODO:就当是获取选做题试题列表吧,后面依据实际情况修改
+      const res = await getOptionalQuestionList(props.subjectId);
+      dataList.value = res || [];
+
+      // 设置已选中的项
+      await nextTick();
+      setSelectedRows();
+    } catch (error) {
+      dataList.value = [];
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 处理选择变化
+  const handleSelectionChange = (selection: OptionalQuestionItem[]) => {
+    const selectedQuestions = selection.map((item) => {
+      return {
+        bigQuestionNo: item.bigQuestionNo,
+        smallQuestionNo: item.smallQuestionNo,
+        intervalScore: item.intervalScore,
+        arbitrationThreshold: item.arbitrationThreshold,
+      };
+    });
+    emit('update:modelValue', selectedQuestions);
+  };
+
+  // 暴露验证方法
+  const validate = () => {
+    if (props.modelValue.length === 0) {
+      ElMessage.warning('请至少选择一个试题');
+      return Promise.reject(new Error('请至少选择一个试题'));
+    }
+    return Promise.resolve(true);
+  };
+
+  defineExpose({
+    validate,
+  });
+
+  onMounted(() => {
+    if (props.subjectId) {
+      getList();
+    }
+  });
+</script>

+ 274 - 0
src/views/mark/group-edit/GroupRuleForm.vue

@@ -0,0 +1,274 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formModel"
+    :rules="formRules"
+    label-width="120px"
+  >
+    <el-form-item label="分组序号" prop="groupNo">
+      <el-input-number
+        v-model="formModel.groupNo"
+        :min="1"
+        :max="99"
+        :step="1"
+        :precision="0"
+        :controls="false"
+        step-strictly
+        placeholder="请输入"
+        :readonly="isEdit || isReset"
+      />
+    </el-form-item>
+
+    <el-form-item v-if="isEdit" label="名称">
+      <el-input v-model="formModel.name" :readonly="isEdit"></el-input>
+    </el-form-item>
+
+    <el-form-item label="图片显示">
+      <el-button type="primary" link @click="onSetMarkingArea">设置</el-button>
+    </el-form-item>
+
+    <template v-if="isReset || isAdd">
+      <el-form-item label="双评">
+        <el-checkbox
+          v-model="formModel.doubleMarking"
+          @change="onDoubleMarkingChange"
+        ></el-checkbox>
+      </el-form-item>
+
+      <template v-if="formModel.doubleMarking">
+        <el-form-item label="仲裁方式" prop="arbitrationType">
+          <el-select
+            v-model="formModel.arbitrationType"
+            placeholder="请选择"
+            clearable
+            style="width: 200px"
+          >
+            <el-option
+              v-for="(val, key) in ARBITRATION_TYPE"
+              :key="key"
+              :label="val"
+              :value="key"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="双评比例" prop="doubleMarkingRatio">
+          <el-input-number
+            v-model="formModel.doubleMarkingRatio"
+            :min="0"
+            :max="1"
+            :step="0.01"
+            :precision="2"
+            :controls="false"
+            step-strictly
+            placeholder="请输入"
+          />
+        </el-form-item>
+        <!-- 按组仲裁 阀值 -->
+        <el-form-item
+          v-if="formModel.arbitrationType === 'GROUP'"
+          label="仲裁阀值"
+          prop="arbitrationThreshold"
+        >
+          <el-input-number
+            v-model="formModel.arbitrationThreshold"
+            :min="0"
+            :max="9999"
+            :step="0.1"
+            :precision="1"
+            :controls="false"
+            step-strictly
+            placeholder="请输入"
+          />
+        </el-form-item>
+        <!-- 小题仲裁 阀值 -->
+        <template v-if="formModel.arbitrationType === 'QUESTION'">
+          <template
+            v-for="(question, qindex) in formModel.questions"
+            :key="`a${qindex}`"
+          >
+            <el-form-item
+              :label="`${question.bigQuestionNo}-${question.smallQuestionNo}仲裁阀值`"
+              :prop="'questions.' + qindex + '.arbitrationThreshold'"
+              :rules="{
+                required: true,
+                message: '请输入仲裁阀值',
+                trigger: 'change',
+              }"
+            >
+              <el-input-number
+                v-model="question.arbitrationThreshold"
+                :min="1"
+                :max="999"
+                :step="0.1"
+                :precision="1"
+                :controls="false"
+                step-strictly
+                placeholder="请输入"
+              />
+            </el-form-item>
+          </template>
+        </template>
+
+        <el-form-item label="合分策略" prop="combinationStrategy">
+          <el-select
+            v-model="formModel.combinationStrategy"
+            placeholder="请选择"
+            clearable
+            style="width: 200px"
+          >
+            <el-option
+              v-for="(val, key) in COMBINE_SCORE_STRATEGY"
+              :key="key"
+              :label="val"
+              :value="key"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="三评规则" prop="threeMarkingRule">
+          <el-select
+            v-model="formModel.threeMarkingRule"
+            placeholder="请选择"
+            clearable
+            style="width: 200px"
+          >
+            <el-option
+              v-for="(val, key) in THREE_EVALUATION_RULE"
+              :key="key"
+              :label="val"
+              :value="key"
+            />
+          </el-select>
+        </el-form-item>
+      </template>
+
+      <el-form-item v-if="isReset" label="重要提示">
+        <el-alert
+          title="保存后分组关联所有评卷任务都将删除,并生成新的分组任务"
+          type="error"
+          effect="dark"
+        />
+      </el-form-item>
+    </template>
+
+    <template v-if="isEdit && formModel.questions">
+      <template
+        v-for="(question, qindex) in formModel.questions"
+        :key="`i${qindex}`"
+      >
+        <el-form-item
+          :label="`${question.bigQuestionNo}-${question.smallQuestionNo}间隔分`"
+          :prop="'questions.' + qindex + '.intervalScore'"
+          :rules="{
+            required: true,
+            message: '请输入间隔分',
+            trigger: 'change',
+          }"
+        >
+          <el-input-number
+            v-model="question.intervalScore"
+            :min="1"
+            :max="999"
+            :step="0.1"
+            :precision="1"
+            :controls="false"
+            step-strictly
+            placeholder="请输入"
+          />
+        </el-form-item>
+      </template>
+    </template>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch, onMounted, computed } from 'vue';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import type { MarkGroupUpdateParams } from '@/api/types/mark';
+  import {
+    COMBINE_SCORE_STRATEGY,
+    ARBITRATION_TYPE,
+    THREE_EVALUATION_RULE,
+  } from '@/constants/enumerate';
+  import { deepCopy } from '@/utils/utils';
+
+  defineOptions({
+    name: 'GroupParamEdit',
+  });
+
+  interface Props {
+    modelValue: MarkGroupUpdateParams;
+    editType: 'add' | 'edit' | 'reset';
+  }
+
+  const props = defineProps<Props>();
+  const emit = defineEmits<{
+    'update:modelValue': [value: MarkGroupUpdateParams];
+  }>();
+
+  const isEdit = computed(() => props.editType === 'edit');
+  const isReset = computed(() => props.editType === 'reset');
+  const isAdd = computed(() => props.editType === 'add');
+
+  const formRef = ref<FormInstance>();
+  const formModel = ref<MarkGroupUpdateParams>({ ...props.modelValue });
+
+  const formRules: FormRules<keyof MarkGroupUpdateParams> = {
+    groupNo: [{ required: true, message: '请输入分组序号', trigger: 'change' }],
+    name: [{ required: true, message: '请输入分组名称', trigger: 'change' }],
+    arbitrationType: [{ required: true, message: '请选择', trigger: 'change' }],
+    combinationStrategy: [
+      { required: true, message: '请选择', trigger: 'change' },
+    ],
+    threeMarkingRule: [
+      { required: true, message: '请选择', trigger: 'change' },
+    ],
+    doubleMarkingRatio: [
+      { required: true, message: '请输入', trigger: 'change' },
+    ],
+    arbitrationThreshold: [
+      { required: true, message: '请输入', trigger: 'change' },
+    ],
+  };
+
+  function onDoubleMarkingChange(val: boolean) {
+    if (!val) {
+      formModel.value.arbitrationType = 'GROUP';
+      formModel.value.combinationStrategy = 'AVG_SCORE';
+      formModel.value.threeMarkingRule = 'CLOSE';
+      formModel.value.doubleMarkingRatio = undefined;
+      formModel.value.arbitrationThreshold = undefined;
+      formModel.value.questions.forEach((item) => {
+        item.arbitrationThreshold = undefined;
+      });
+    }
+  }
+
+  function onSetMarkingArea() {
+    // TODO: 实现图片显示设置
+    console.log('onSetMarkingArea');
+  }
+
+  // 监听表单数据变化,同步到父组件
+  watch(
+    formModel,
+    (newVal) => {
+      emit('update:modelValue', newVal);
+    },
+    { deep: true }
+  );
+
+  // 暴露验证方法
+  const validate = () => {
+    return formRef.value?.validate();
+  };
+
+  defineExpose({
+    validate,
+  });
+
+  onMounted(() => {
+    formModel.value = deepCopy(props.modelValue);
+  });
+</script>