Selaa lähdekoodia

自由知识点组卷

jinxia.mo 1 kuukausi sitten
vanhempi
commit
f5d810db81

+ 46 - 78
back-ui/src/views/dz/cards/components/EditDialog.vue

@@ -125,10 +125,10 @@ import IeSelect from '@/components/IeSelect/index.vue';
 import IeUniversitySelect from '@/components/IeUniversitySelect/index.vue';
 import DirectionDialog from './DirectionDialog.vue';
 import { updateCardUser, getUserByCardId } from '@/api/dz/cards';
-import { listAllSubject } from '@/api/dz/subject';
 import { getCurrentInstance, nextTick, watch, watchEffect } from 'vue';
 import draggable from 'vuedraggable';
 import { Rank, Close } from '@element-plus/icons-vue';
+import useProvinceExamTypeSubject from '@/views/dz/hooks/useProvinceExamTypeSubject.js';
 
 const { proxy } = getCurrentInstance();
 
@@ -140,26 +140,49 @@ const form = ref({
   directionStudy: []
 })
 
-// 专业类别相关
-const examMajorList = ref([]);
-const selectedExamMajor = ref(null);
-
+// 使用通用的省份-考生类型-科目联动 hook(专业类别即科目,只在VHS时显示)
 const {
-  reset,
   area,
   getAreaList,
+  selectedExamType,
+  examTypeList,
+  selectedSubjectId,
+  subjectList,
+  reset: resetProvinceExamTypeSubject,
+  initData: initProvinceExamTypeSubject
+} = useProvinceExamTypeSubject({
+  autoLoadArea: false,
+  loadExamType: true,
+  onlyVHS: true  // 只在考生类型为 VHS 时加载科目
+})
+
+// 专业类别相关(使用 selectedSubjectId 和 subjectList)
+const examMajorList = computed(() => {
+  // 转换数据格式,dictValue 对应 subject_id,dictLabel 对应 subject_name
+  return subjectList.value.map(item => ({
+    dictValue: Number(item.subjectId),
+    dictLabel: item.subjectName
+  }))
+})
+
+const selectedExamMajor = computed({
+  get: () => selectedSubjectId.value ? Number(selectedSubjectId.value) : null,
+  set: (val) => {
+    selectedSubjectId.value = val ? Number(val) : null
+  }
+})
+
+const {
   schoolList,
   selectedSchool,
   classList,
   selectedClass,
-  examTypeList,
-  selectedExamType,
-  getExamTypeList,
   campusList,
   selectedCampus,
   campusClassList,
   selectedCampusClass,
-} = useSchool({ autoLoad: false, loadExamType: true, loadClass: true, loadCampus: true, loadCampusClass: true });
+  reset: resetSchool
+} = useSchool({ autoLoad: false, loadClass: true, loadCampus: true, loadCampusClass: true });
 
 watchEffect(() => {
   form.value.examType = selectedExamType.value;
@@ -175,54 +198,10 @@ watchEffect(() => {
   } else {
     form.value.examMajorName = null;
   }
+  // 同步 location
+  form.value.location = area.selectedItem?.shortName || area.selectedItem?.areaName?.replace('省', '') || null;
 });
 
-// 监听考生类型和省份变化,加载专业类别列表
-watch([selectedExamType, () => form.value.location], ([examType, location], [oldExamType, oldLocation]) => {
-  // 避免初始化时执行(第一次加载时 oldExamType 和 oldLocation 都是 undefined)
-  if (oldExamType === undefined && oldLocation === undefined) {
-    return;
-  }
-  // 只有 location 与 examType 有值或这2个值有变化时才调用
-  if (examType && location) {
-    // 如果考生类型变为 VHS,加载专业类别列表
-    if (examType === 'VHS') {
-      loadExamMajorList(location, examType);
-    } else {
-      // 如果考生类型不是 VHS,清空专业类别相关数据
-      examMajorList.value = [];
-      selectedExamMajor.value = null;
-      form.value.examMajor = null;
-      form.value.examMajorName = null;
-    }
-  } else {
-    // 如果 location 或 examType 为空,清空专业类别相关数据
-    examMajorList.value = [];
-    selectedExamMajor.value = null;
-    form.value.examMajor = null;
-    form.value.examMajorName = null;
-  }
-});
-
-// 加载专业类别列表
-const loadExamMajorList = async (location, examType) => {
-  try {
-    const res = await listAllSubject({
-      locations: location,
-      examTypes: examType
-    });
-    // 转换数据格式,dictValue 对应 subject_id,dictLabel 对应 subject_name
-    // 确保 dictValue 是 Number 类型,与 examMajor 的 Integer 类型匹配
-    examMajorList.value = (res.data || []).map(item => ({
-      dictValue: Number(item.subjectId),
-      dictLabel: item.subjectName
-    }));
-  } catch (error) {
-    console.error('加载专业类别列表失败:', error);
-    examMajorList.value = [];
-  }
-};
-
 
 
 const rules = ref({
@@ -240,15 +219,12 @@ const rules = ref({
 })
 
 const handleBeforeClose = () => {
-  reset();
+  resetSchool();
+  resetProvinceExamTypeSubject();
   form.value = {
     scores: {},
     directionStudy: []
   };
-  examMajorList.value = [];
-  selectedExamMajor.value = null;
-  form.value.examMajor = null;
-  form.value.examMajorName = null;
 }
 
 const open = (cardInfo) => {
@@ -275,23 +251,15 @@ const getUserInfo = (cardInfo) => {
       outDate,
       directionStudy: res.data.directionStudy || []
     };
-    const areaList = await getAreaList();
-    // 只有名称,没有 id,所以需要手动查找赋值
-    const targetArea = areaList.find(item => item.areaName === location + '省');
-    if (targetArea) {
-      area.list = areaList;
-      area.selected = targetArea.areaId;
-      area.selectedItem = targetArea;
-    }
-    // 初始化时设置 examMajor 值(不调用接口,由 watch 监听变化后调用)
-    // 确保类型一致,将 examMajor 转换为 Number 类型
-    if (res.data.examMajor != null) {
-      selectedExamMajor.value = Number(res.data.examMajor);
-    } else {
-      selectedExamMajor.value = null;
-    }
-    // 设置考生类型和省份(这会触发 watch,当 location 和 examType 都有值时会调用接口)
-    selectedExamType.value = res.data.examType;
+    
+    // 使用通用 hook 的 initData 方法初始化省份、考生类型、科目
+    await initProvinceExamTypeSubject({
+      location: location,
+      examType: res.data.examType,
+      subjectId: res.data.examMajor
+    })
+    
+    // 设置其他字段
     selectedCampus.value = res.data.campusSchoolId;
     selectedCampusClass.value = res.data.campusClassId;
     selectedSchool.value = res.data.schoolId;

+ 231 - 0
back-ui/src/views/dz/hooks/useProvinceExamTypeSubject.js

@@ -0,0 +1,231 @@
+import { ref, watch, computed, nextTick } from 'vue'
+import useSchool from '@/hooks/useSchool'
+import { listAllSubject } from '@/api/dz/subject'
+
+/**
+ * 通用的省份-考生类型-科目联动 Hook
+ * @param {Object} options - 配置选项
+ * @param {Boolean} options.autoLoadArea - 是否自动加载省份列表,默认 true
+ * @param {Boolean} options.loadExamType - 是否加载考生类型,默认 true
+ * @param {Boolean} options.onlyVHS - 是否只在考生类型为 VHS 时加载科目,默认 false
+ * @returns {Object} 返回响应式数据和方法
+ */
+export function useProvinceExamTypeSubject(options = {}) {
+  const {
+    autoLoadArea = true,
+    loadExamType = true,
+    onlyVHS = false
+  } = options
+
+  // 使用 useSchool hook 获取省份和考生类型相关功能
+  const {
+    area,
+    getAreaList,
+    examTypeList,
+    selectedExamType,
+    getExamTypeList,
+    reset: resetSchool
+  } = useSchool({ 
+    autoLoad: autoLoadArea, 
+    loadExamType: loadExamType 
+  })
+
+  // 科目相关
+  const subjectList = ref([])
+  const selectedSubjectId = ref(null)
+  
+  // 初始化标志,用于标识是否正在初始化数据
+  const isInitializing = ref(false)
+
+  // 计算 location(从 area.selectedItem 获取)
+  const location = computed(() => {
+    return area.selectedItem?.shortName || area.selectedItem?.areaName?.replace('省', '') || null
+  })
+
+  // 监听省份变化,重新加载考生类型
+  watch(() => area.selected, async (newVal) => {
+    // 如果正在初始化,不清空考生类型
+    if (isInitializing.value) {
+      return
+    }
+    
+    if (newVal && loadExamType) {
+      // 省份变化时,重新加载考生类型列表
+      await getExamTypeList()
+      // 清空已选择的考生类型和科目
+      selectedExamType.value = null
+      selectedSubjectId.value = null
+      subjectList.value = []
+    } else if (!newVal) {
+      // 清空省份时,清空考生类型和科目
+      selectedExamType.value = null
+      selectedSubjectId.value = null
+      subjectList.value = []
+    }
+  })
+
+  // 监听考生类型和省份变化,加载科目列表
+  watch([selectedExamType, location], async ([examType, loc], [oldExamType, oldLocation]) => {
+    // 避免初始化时执行
+    if (oldExamType === undefined && oldLocation === undefined) {
+      return
+    }
+
+    // 只有 location 与 examType 都有值时才调用
+    if (examType && loc) {
+      // 如果设置了 onlyVHS,只有考生类型为 VHS 时才加载
+      if (onlyVHS && examType !== 'VHS') {
+        subjectList.value = []
+        selectedSubjectId.value = null
+        return
+      }
+      
+      // 加载科目列表
+      await loadSubjectList(loc, examType)
+    } else {
+      // 如果 location 或 examType 为空,清空科目列表
+      subjectList.value = []
+      selectedSubjectId.value = null
+    }
+  })
+
+  /**
+   * 加载科目列表
+   * @param {String} loc - 省份名称
+   * @param {String} examType - 考生类型
+   */
+  const loadSubjectList = async (loc, examType) => {
+    try {
+      const res = await listAllSubject({
+        locations: loc,
+        examTypes: examType
+      })
+      
+      // 转换数据格式
+      subjectList.value = (res.data || []).map(item => ({
+        subjectId: item.subjectId,
+        subjectName: item.subjectName,
+        locations: item.locations,
+        examTypes: item.examTypes
+      }))
+      
+      // 如果当前选中的科目不在新列表中,清空选择
+      if (selectedSubjectId.value) {
+        const exists = subjectList.value.some(s => s.subjectId === selectedSubjectId.value)
+        if (!exists) {
+          selectedSubjectId.value = null
+        }
+      }
+    } catch (error) {
+      console.error('加载科目列表失败:', error)
+      subjectList.value = []
+      selectedSubjectId.value = null
+    }
+  }
+
+  /**
+   * 重置所有数据
+   */
+  const reset = () => {
+    resetSchool()
+    subjectList.value = []
+    selectedSubjectId.value = null
+  }
+
+  /**
+   * 初始化数据(用于编辑场景)
+   * @param {Object} data - 初始数据
+   * @param {String} data.location - 省份名称
+   * @param {String} data.examType - 考生类型
+   * @param {Number} data.subjectId - 科目ID
+   */
+  const initData = async (data = {}) => {
+    // 设置初始化标志,防止 watch 清空数据
+    isInitializing.value = true
+    
+    try {
+      // 保存要设置的考生类型值
+      const examTypeToSet = data.examType
+      
+      // 如果有 location,设置省份
+      if (data.location) {
+        const areaList = await getAreaList()
+        // 查找匹配的省份(支持带"省"或不带"省")
+        const targetArea = areaList.find(item => {
+          const areaName = item.areaName?.replace('省', '')
+          return areaName === data.location || item.areaName === data.location + '省'
+        })
+        if (targetArea) {
+          area.list = areaList
+          area.selected = targetArea.areaId
+          area.selectedItem = targetArea
+          
+          // 等待考生类型列表加载完成(如果 loadExamType 为 true)
+          if (loadExamType) {
+            await getExamTypeList()
+            // 等待一下,确保 examTypeList 已经更新
+            await nextTick()
+          }
+        }
+      }
+
+      // 设置考生类型(在考生类型列表加载完成后设置)
+      if (examTypeToSet) {
+        selectedExamType.value = examTypeToSet
+        // 等待一下,确保 watch 执行完成
+        await nextTick()
+      }
+
+      // 设置科目ID(需要在科目列表加载完成后设置)
+      if (data.subjectId) {
+        // 等待科目列表加载(最多等待3秒)
+        let resolved = false
+        await new Promise((resolve) => {
+          const timeout = setTimeout(() => {
+            if (!resolved) {
+              resolved = true
+              resolve()
+            }
+          }, 3000)
+          
+          const unwatch = watch(subjectList, (newList) => {
+            if (newList.length > 0 && !resolved) {
+              resolved = true
+              clearTimeout(timeout)
+              selectedSubjectId.value = Number(data.subjectId)
+              unwatch()
+              resolve()
+            }
+          }, { immediate: true })
+        })
+      }
+    } finally {
+      // 初始化完成,清除标志
+      isInitializing.value = false
+    }
+  }
+
+  return {
+    // 省份相关
+    area,
+    getAreaList,
+    location,
+    
+    // 考生类型相关
+    examTypeList,
+    selectedExamType,
+    getExamTypeList,
+    
+    // 科目相关
+    subjectList,
+    selectedSubjectId,
+    loadSubjectList,
+    
+    // 方法
+    reset,
+    initData
+  }
+}
+
+export default useProvinceExamTypeSubject
+

+ 176 - 0
back-ui/src/views/dz/papers/components/paper-knowledge-hand.vue

@@ -0,0 +1,176 @@
+<template>
+    <el-row :gutter="20">
+        <el-col :span="24">
+            <el-form label-width="68px">
+<!--                <BatchYearSelect v-model:batch-id="batchId" :batch-list="batchList" />-->
+                <el-row :gutter="20">
+                    <el-col :span="8">
+                        <el-form-item label="省份">
+                            <ie-select 
+                                v-model="area.selected" 
+                                v-model:selectedItem="area.selectedItem" 
+                                :options="area.list" 
+                                label-key="areaName" 
+                                value-key="areaId" 
+                                clearable 
+                                filterable
+                                style="width: 100%"
+                                placeholder="请选择省份"/>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="8">
+                        <el-form-item label="考生类型">
+                            <el-select v-model="selectedExamType" clearable filterable style="width: 100%" placeholder="请选择考生类型">
+                                <el-option v-for="e in examTypeList" :label="e.dictLabel" :value="e.dictValue"/>
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="8">
+                        <el-form-item label="科目">
+                            <el-select v-model="selectedSubjectId" clearable filterable style="width: 100%" placeholder="请选择科目">
+                                <el-option v-for="s in subjectList" :label="s.subjectName" :value="s.subjectId"/>
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+            </el-form>
+        </el-col>
+        
+    </el-row>
+    <el-divider />
+    <el-row v-if="!hasBuiltPaper" :gutter="20">
+        <el-col :span="6">
+            <knowledge-tree allow-multiple/>
+        </el-col>
+        <el-col :span="18">
+            <question-intelligent @submit="handleSubmit" />
+        </el-col>
+    </el-row>
+    <built-paper-list ref="built" @send="handleSubmit" />
+</template>
+
+<script setup name="PaperFullIntelligent">
+import { ref, computed, watch } from 'vue'
+import consts from "@/utils/consts.js";
+import {useProvidePaperFullCondition} from "@/views/dz/papers/hooks/usePaperFullCondition.js";
+import {useProvidePaperClassStatisticCondition} from "@/views/dz/papers/hooks/usePaperClassStatisticCondition.js";
+import KnowledgeTree from "@/views/dz/papers/components/plugs/knowledge-tree.vue";
+import {useProvidePaperKnowledgeCondition} from "@/views/dz/papers/hooks/usePaperKnowledgeCondition.js";
+import QuestionIntelligent from "@/views/dz/papers/components/plugs/question-intelligent.vue";
+import {ElMessage} from "element-plus";
+import {buildPaperFullIntelligent} from "@/api/dz/papers.js";
+import BuiltPaperList from "@/views/dz/papers/components/plugs/built-paper-list.vue";
+import IeSelect from '@/components/IeSelect/index.vue'
+import useProvinceExamTypeSubject from '@/views/dz/hooks/useProvinceExamTypeSubject.js'
+
+const type = consts.enums.buildType.FullIntelligent
+const {
+    batchId,
+    batchList,
+    examType: paperExamType,
+    examTypes: paperExamTypes,
+    subjectId: paperSubjectId,
+    groupedSubjects,
+    conditionArgs,
+    onConditionReady,
+    onBatchReady
+} = useProvidePaperFullCondition(type)
+
+// 使用通用的省份-考生类型-科目联动 hook
+const {
+    area,
+    selectedExamType,
+    examTypeList,
+    selectedSubjectId,
+    subjectList
+} = useProvinceExamTypeSubject({
+    autoLoadArea: true,
+    loadExamType: true,
+    onlyVHS: false
+})
+
+// 同步到 paperExamType 和 paperSubjectId(用于原有的条件逻辑)
+watch(selectedExamType, (val) => {
+    paperExamType.value = val || ''
+})
+
+watch(selectedSubjectId, (val) => {
+    paperSubjectId.value = val || ''
+})
+const {selectedClasses, classList, loadClassStatistic} = useProvidePaperClassStatisticCondition()
+const {knowledgeNode, knowledgeCheckNodes, loadKnowledge} = useProvidePaperKnowledgeCondition()
+const built = ref(null)
+const hasBuiltPaper = computed(() => built.value?.hasPaper)
+
+const handleSubmit = async (qTypes) => {
+    // validation
+    if (!batchId.value) return ElMessage.error('请选择批次')
+    if (!knowledgeCheckNodes.value.length) return ElMessage.error('请选择知识点')
+    if (!qTypes.value.length || qTypes.value.every(t => !t.count)) return ElMessage.error('请填写题量')
+    const classIds = selectedClasses.value.map(c => c.classId)
+    if (!classIds.length) return ElMessage.error('请选择班级')
+
+    // build
+    const commit = {
+        buildType: type,
+        batchId: toValue(batchId),
+        examType: toValue(paperExamType),
+        subjectId: toValue(paperSubjectId),
+        knowledgeIds: knowledgeCheckNodes.value.map(k => k.id),
+        types: qTypes.value.map(t => ({
+            type: t.dictValue,
+            title: t.dictLabel,
+            count: t.count
+        })),
+        classIds
+    }
+    await buildPaperFullIntelligent(commit)
+    ElMessage.success('生成成功')
+    _loadClassStatistic()
+}
+
+let statArg = null
+const _loadClassStatistic = async () => {
+    selectedClasses.value = []
+    classList.value = []
+
+    await loadClassStatistic(statArg)
+}
+onConditionReady(async (payload) => {
+    statArg = payload
+    knowledgeNode.value = null
+    knowledgeCheckNodes.value = []
+
+    await loadKnowledge(payload)
+    await _loadClassStatistic()
+    await built.value.loadBuiltPaper(payload)
+})
+
+// 监听批次变化,调用接口
+onBatchReady(async (payload) => {
+    // 如果已经有完整的条件参数,更新批次ID;否则只使用批次
+    if (statArg) {
+        statArg = { ...statArg, batchId: payload.batchId }
+    } else {
+        statArg = payload
+    }
+    await _loadClassStatistic()
+    await built.value.loadBuiltPaper(statArg)
+})
+
+watch(conditionArgs, () => built.value.reset())
+
+// 刷新数据
+const refresh = async () => {
+    if (statArg) {
+        await _loadClassStatistic()
+        await built.value.loadBuiltPaper(statArg)
+    }
+}
+
+defineExpose({ refresh })
+</script>
+
+<style scoped>
+
+</style>

+ 8 - 1
back-ui/src/views/dz/papers/index.vue

@@ -26,6 +26,7 @@ import PaperExactIntelligent from "@/views/dz/papers/components/paper-exact-inte
 import PaperFullIntelligent from "@/views/dz/papers/components/paper-full-intelligent.vue";
 import PaperExactHand from "@/views/dz/papers/components/paper-exact-hand.vue";
 import PaperFullHand from "@/views/dz/papers/components/paper-full-hand.vue";
+import KnowledgeHand from "@/views/dz/papers/components/paper-knowledge-hand.vue";
 import {Refresh} from "@element-plus/icons-vue";
 
 const {loading} = useProvideGlobalLoading()
@@ -50,7 +51,13 @@ const tabs = ref([{
     label: '全量手动',
     page: markRaw(PaperFullHand),
     visited: false
-}])
+}, {
+    name: 'KnowledgeHand',
+    label: '知识点',
+    page: markRaw(KnowledgeHand),
+    visited: false
+}
+])
 const currentTab = ref('ExactIntelligent')
 
 // 存储组件实例的引用