Browse Source

new paper build

abpcoder 3 tuần trước cách đây
mục cha
commit
52e52ef936
32 tập tin đã thay đổi với 1117 bổ sung24 xóa
  1. 120 2
      back-ui/src/api/dz/papers.js
  2. 1 1
      back-ui/src/components/Table/index.vue
  3. 10 0
      back-ui/src/utils/consts.js
  4. 0 0
      back-ui/src/views/dz/papers/components-bak/paper-by-hand.vue
  5. 0 0
      back-ui/src/views/dz/papers/components-bak/paper-by-intelligent.vue
  6. 0 0
      back-ui/src/views/dz/papers/components-bak/paper-exact-conditions.vue
  7. 0 0
      back-ui/src/views/dz/papers/components-bak/paper-full-conditions.vue
  8. 0 0
      back-ui/src/views/dz/papers/components-bak/paper-hand-exact.vue
  9. 0 0
      back-ui/src/views/dz/papers/components-bak/paper-hand-full.vue
  10. 0 0
      back-ui/src/views/dz/papers/components-bak/paper-intelligent-exact.vue
  11. 0 0
      back-ui/src/views/dz/papers/components-bak/paper-intelligent-full.vue
  12. 0 0
      back-ui/src/views/dz/papers/components-bak/paper-knowledge-tree.vue
  13. 26 0
      back-ui/src/views/dz/papers/components-bak/paper-publish.vue
  14. 0 0
      back-ui/src/views/dz/papers/components-bak/paper-question-hand.vue
  15. 3 3
      back-ui/src/views/dz/papers/components-bak/paper-question-intelligent.vue
  16. 31 0
      back-ui/src/views/dz/papers/components/conditions/usePaperBatchCondition.js
  17. 25 0
      back-ui/src/views/dz/papers/components/conditions/usePaperClassStatisticCondition.js
  18. 81 0
      back-ui/src/views/dz/papers/components/conditions/usePaperExactCondition.js
  19. 49 0
      back-ui/src/views/dz/papers/components/conditions/usePaperFullCondition.js
  20. 35 0
      back-ui/src/views/dz/papers/components/conditions/usePaperKnowledgeCondition.js
  21. 111 0
      back-ui/src/views/dz/papers/components/conditions/usePaperQuestionCondition.js
  22. 10 0
      back-ui/src/views/dz/papers/components/paper-exact-hand.vue
  23. 69 0
      back-ui/src/views/dz/papers/components/paper-exact-intelligent.vue
  24. 10 0
      back-ui/src/views/dz/papers/components/paper-full-hand.vue
  25. 112 0
      back-ui/src/views/dz/papers/components/paper-full-intelligent.vue
  26. 48 0
      back-ui/src/views/dz/papers/components/plugs/class-statistic-table.vue
  27. 42 0
      back-ui/src/views/dz/papers/components/plugs/knowledge-tree.vue
  28. 45 0
      back-ui/src/views/dz/papers/components/plugs/question-intelligent.vue
  29. 1 1
      back-ui/src/views/dz/papers/hooks/usePaperQuestionCondition.js
  30. 37 13
      back-ui/src/views/dz/papers/index.vue
  31. 249 2
      back-ui/src/views/dz/papers/list.vue
  32. 2 2
      back-ui/src/views/hooks/useGlobalLoading.js

+ 120 - 2
back-ui/src/api/dz/papers.js

@@ -2,6 +2,8 @@ import request from '@/utils/request'
 
 /// 科目列表
 export function getPaperSubjects(query) {
+    // 2025.10.16 科目更新和组卷类型有关系
+    // query: {buildType}
     return request({
         url: '/learn/teaching/subjects',
         method: 'get',
@@ -11,6 +13,11 @@ export function getPaperSubjects(query) {
 
 /// 科目列表
 export function getPaperKnowledges(query) {
+    // 2025.10.16 科目更新和组卷类型有关系
+    // query: {
+    // 定向条件:universityId,majorId,majorPlanId
+    // 全量条件:examType,subjectId
+    // }
     return request({
         url: '/learn/teaching/knowledges',
         method: 'get',
@@ -29,11 +36,11 @@ export function getPaperProvinces(query) {
 
 /// 考生类型
 export function getPaperExamTypes(query) {
-    // query: {location}
+    // query: {} // 2025.10.16 科目更新和组卷类型有关系,应该不需要参数了
     return request({
         url: '/learn/teaching/examTypes',
         method: 'get',
-        params: query
+        params: {location: '湖南', ...query}
     })
 }
 
@@ -47,6 +54,16 @@ export function getPaperUniversities(query) {
     })
 }
 
+/// 专业组
+export function getPaperMajorGroups(query) {
+    // query: {location, examType, batchId, universityId}
+    return request({
+        url: '/learn/teaching/majorGroups',
+        method: 'get',
+        params: query
+    })
+}
+
 /// 专业组
 export function getPaperMajors(query) {
     // query: {location, examType, batchId, universityId}
@@ -59,6 +76,15 @@ export function getPaperMajors(query) {
 
 /// 试卷批次
 export function getPaperBatches() {
+    // TODO: remove test code
+    return Promise.resolve({
+        code: 200,
+        data: [
+            {name: '第二批', batchId: 2},
+            {name: '第一批', batchId: 1}
+        ]
+    })
+
     return request({
         url: '/learn/test/list',
         method: 'get'
@@ -139,3 +165,95 @@ export function publishPapers(data) {
     })
 }
 
+export function getPaperList(query) {
+    return request({
+        url: '/learn/teaching/papers',
+        method: 'get',
+        params: query
+    })
+}
+
+export function getPaperDetail(query) {
+    return request({
+        url: '/learn/teaching/paper',
+        method: 'get',
+        params: query
+    })
+}
+
+export function getPaperClassStatistic(query) {
+    // TODO: remove test code
+    return Promise.resolve({
+        code: 200,
+        data: [
+            {classId: 0, className: '2501班', total: 50, unexact: 10, exact: 40, unsend: 10, send: 20, unfinish: 5, finish: 15},
+            {classId: 0, className: '2502班', total: 50, unexact: 10, exact: 40, unsend: 10, send: 20, unfinish: 5, finish: 15},
+            {classId: 0, className: '2503班', total: 50, unexact: 10, exact: 40, unsend: 10, send: 20, unfinish: 5, finish: 15},
+            {classId: 0, className: '2504班', total: 50, unexact: 10, exact: 40, unsend: 10, send: 20, unfinish: 5, finish: 15}
+        ]
+    })
+    // 2025.10.16 新增班级统计,用于组卷生成
+    const queryDemo = {
+        buildType: '', // 试卷构建类型,定义枚举?
+        batchId: '', // 批次
+        examType: '',
+        subjectId: '',
+        universityId: '',
+        majorGroup: '',
+        majorPlanId: ''
+    }
+    const responseDemo = {
+        classId: '',
+        className: '',
+        total: '',
+        built: '',
+        sent: '',
+        finished: '',
+        exact: '',
+        exactNew: '',
+        lastBuiltTime: '',
+        lastSentTime: ''
+    }
+
+    return request({
+        url: '/learn/teaching/classStatistic',
+        method: 'get',
+        params: query
+    })
+}
+
+export function buildPaperExactIntelligent(data) {
+    // {buildType, batchId, classIds}
+    return request({
+        url: '/learn/teaching/build/exactIntelligent',
+        method: 'post',
+        data
+    })
+}
+
+export function buildPaperFullIntelligent(data) {
+    // {buildType, batchId, examType, subjectId, classIds}
+    return request({
+        url: '/learn/teaching/build/fullIntelligent',
+        method: 'post',
+        data
+    })
+}
+
+export function buildPaperExactHand(data) {
+    // {buildType, batchId, universityId, majorId, majorPlanId, classIds, questions}
+    return request({
+        url: '/learn/teaching/build/exactHand',
+        method: 'post',
+        data
+    })
+}
+
+export function buildPaperFullHand(data) {
+    // {buildType, batchId, examType, subjectId, questions}
+    return request({
+        url: '/learn/teaching/build/fullHand',
+        method: 'post',
+        data
+    })
+}

+ 1 - 1
back-ui/src/components/Table/index.vue

@@ -171,7 +171,7 @@
   </div>
 </template>
 
-<script setup>
+<script setup name="Table">
 import { ref, computed, watch } from "vue";
 import { parseTime } from "@/utils/ruoyi";
 // import { useDict } from '@/stores/modules/dict' // 不需要,父组件传递

+ 10 - 0
back-ui/src/utils/consts.js

@@ -0,0 +1,10 @@
+export default {
+    enums: {
+        buildType: {
+            ExactIntelligent: 'ExactIntelligent',
+            FullIntelligent: 'FullIntelligent',
+            ExactHand: 'ExactHand',
+            FullHand: 'ExactHand'
+        }
+    }
+}

+ 0 - 0
back-ui/src/views/dz/papers/components/paper-by-hand.vue → back-ui/src/views/dz/papers/components-bak/paper-by-hand.vue


+ 0 - 0
back-ui/src/views/dz/papers/components/paper-by-intelligent.vue → back-ui/src/views/dz/papers/components-bak/paper-by-intelligent.vue


+ 0 - 0
back-ui/src/views/dz/papers/components/paper-exact-conditions.vue → back-ui/src/views/dz/papers/components-bak/paper-exact-conditions.vue


+ 0 - 0
back-ui/src/views/dz/papers/components/paper-full-conditions.vue → back-ui/src/views/dz/papers/components-bak/paper-full-conditions.vue


+ 0 - 0
back-ui/src/views/dz/papers/components/paper-hand-exact.vue → back-ui/src/views/dz/papers/components-bak/paper-hand-exact.vue


+ 0 - 0
back-ui/src/views/dz/papers/components/paper-hand-full.vue → back-ui/src/views/dz/papers/components-bak/paper-hand-full.vue


+ 0 - 0
back-ui/src/views/dz/papers/components/paper-intelligent-exact.vue → back-ui/src/views/dz/papers/components-bak/paper-intelligent-exact.vue


+ 0 - 0
back-ui/src/views/dz/papers/components/paper-intelligent-full.vue → back-ui/src/views/dz/papers/components-bak/paper-intelligent-full.vue


+ 0 - 0
back-ui/src/views/dz/papers/components/paper-knowledge-tree.vue → back-ui/src/views/dz/papers/components-bak/paper-knowledge-tree.vue


+ 26 - 0
back-ui/src/views/dz/papers/components-bak/paper-publish.vue

@@ -0,0 +1,26 @@
+<template>
+    <div class="">
+
+    </div>
+</template>
+
+<script setup name="PaperPublish">
+
+import {getPaperClasses} from "@/api/dz/papers.js";
+
+const props = defineProps({
+    paper: Object
+})
+
+const classList = ref([])
+const studentContainer = ref({})
+
+watch(() => paper.id, async (paperId) => {
+    const res = await getPaperClasses()
+    classList.value = res.data
+})
+</script>
+
+<style scoped>
+
+</style>

+ 0 - 0
back-ui/src/views/dz/papers/components/paper-question-hand.vue → back-ui/src/views/dz/papers/components-bak/paper-question-hand.vue


+ 3 - 3
back-ui/src/views/dz/papers/components/paper-question-intelligent.vue → back-ui/src/views/dz/papers/components-bak/paper-question-intelligent.vue

@@ -24,7 +24,7 @@
 import {useInjectPaperExactCondition} from "@/views/dz/papers/hooks/usePaperExactCondition.js";
 import {useInjectPaperFullCondition} from "@/views/dz/papers/hooks/usePaperFullCondition.js";
 import {useProvidePaperQuestionCondition} from "@/views/dz/papers/hooks/usePaperQuestionCondition.js";
-import {useInjectLoading} from "@/views/hooks/useLoading.js";
+import {useInjectLoading} from "@/views/hooks/useGlobalLoading.js";
 import {ElMessage} from "element-plus";
 import {buildPaperAuto} from "@/api/dz/papers.js";
 import {sleep} from "@/utils/index.js";
@@ -61,8 +61,8 @@ const buildPaper = async function () {
     }
     await buildPaperAuto(commit)
     ElMessage.success('生成成功,即将打开组卷记录')
-    sleep(2000)
-    router.push('/paper/list')
+    await sleep(2000)
+    await router.push('/paper/list')
 }
 </script>
 

+ 31 - 0
back-ui/src/views/dz/papers/components/conditions/usePaperBatchCondition.js

@@ -0,0 +1,31 @@
+import {getPaperBatches} from "@/api/dz/papers.js";
+import {createEventHook, injectLocal, provideLocal} from "@vueuse/core";
+
+const key = Symbol('PaperBatchCondition')
+
+export const useProvidePaperBatchCondition = function (type) {
+    // 批次
+    const buildType = type
+    const batchId = ref('')
+    const batchList = ref([])
+    const batchEvent = createEventHook()
+
+    onMounted(async () => {
+        const res = await getPaperBatches()
+        batchList.value = res.data
+    })
+    watch(batchId, async (batchId) => {
+        if (batchId) {
+            const payload = {buildType, batchId}
+            await batchEvent.trigger(payload)
+        }
+    })
+
+    const payload = {buildType, batchId, batchList, onBatchReady: batchEvent.on}
+    provideLocal(key, payload)
+    return payload
+}
+
+export const useInjectPaperBatchCondition = function () {
+    return injectLocal(key)
+}

+ 25 - 0
back-ui/src/views/dz/papers/components/conditions/usePaperClassStatisticCondition.js

@@ -0,0 +1,25 @@
+import {injectLocal, provideLocal} from "@vueuse/core";
+import {getPaperClassStatistic} from "@/api/dz/papers.js";
+
+const key = Symbol('PaperClassStatisticCondition')
+
+export const useProvidePaperClassStatisticCondition = function () {
+    const selectedClasses = ref([])
+    const classList = ref([])
+
+    const loadClassStatistic = async function (payload) {
+        selectedClasses.value = []
+        classList.value = []
+
+        const res = await getPaperClassStatistic(payload)
+        classList.value = res.data
+    }
+
+    const payload = {selectedClasses, classList, loadClassStatistic}
+    provideLocal(key, payload)
+    return payload
+}
+
+export const useInjectPaperClassStatisticCondition = function () {
+    return injectLocal(key)
+}

+ 81 - 0
back-ui/src/views/dz/papers/components/conditions/usePaperExactCondition.js

@@ -0,0 +1,81 @@
+import {useProvidePaperBatchCondition} from "@/views/dz/papers/components/conditions/usePaperBatchCondition.js";
+import {getPaperMajorGroups, getPaperMajors, getPaperUniversities} from "@/api/dz/papers.js";
+import {createEventHook, injectLocal, provideLocal} from "@vueuse/core";
+
+const key = Symbol('PaperExactCondition')
+export const useProvidePaperExactCondition = function (type) {
+    const {buildType, batchId, batchList} = useProvidePaperBatchCondition(type)
+
+    const universityId = ref('')
+    const universities = ref([])
+
+    const majorGroup = ref('')
+    const majorGroups = ref([])
+
+    const majorPlanId = ref('')
+    const majors = ref([])
+
+    const exactEvent = createEventHook()
+    const triggerExactEvent = async function (args) {
+         await exactEvent.trigger(args)
+    }
+
+    watch(batchId, async (batchId) => {
+        universityId.value = ''
+        universities.value = []
+
+        if (!batchId) return
+        const res = await getPaperUniversities({buildType, batchId})
+        universities.value = res.data
+    })
+
+    watch([batchId, universityId], async ([batchId, universityId]) => {
+        majorGroup.value = ''
+        majorGroups.value = []
+
+        if (!batchId || !universityId) return
+        const res = await getPaperMajorGroups({buildType, batchId, universityId})
+        majorGroups.value = res.data
+
+        // 没有数据时,说明已经满足了条件
+        if (!res.data.length) await triggerExactEvent({buildType, batchId, universityId})
+    })
+
+    watch([batchId, universityId, majorGroup], async ([batchId, universityId, majorGroup]) => {
+        majorPlanId.value = ''
+        majors.value = []
+
+        if (!batchId || !universityId || !majorGroup) return
+        const res = await getPaperMajors({buildType, batchId, universityId, majorGroup})
+        majors.value = res.data
+
+        // 没有数据时,说明已经满足了条件
+        if (!res.data.length) await triggerExactEvent({buildType, batchId, universityId, majorGroup})
+    })
+
+    watch(majorPlanId, async (majorPlanId) => {
+        if (!majorPlanId) return
+
+        // 最细的情况,选中了专业
+        const args = {
+            buildType,
+            batchId: toValue(batchId),
+            universityId: toValue(universityId),
+            majorGroup: toValue(majorGroup),
+            majorPlanId
+        }
+        await triggerExactEvent(args)
+    })
+
+    const payload = {buildType, batchId, batchList,
+        universityId, universities,
+        majorGroup, majorGroups,
+        majorPlanId, majors,
+        onExactReady: exactEvent.on}
+    provideLocal(key, payload)
+    return payload
+}
+
+export const useInjectPaperExactCondition = function () {
+    return injectLocal(key)
+}

+ 49 - 0
back-ui/src/views/dz/papers/components/conditions/usePaperFullCondition.js

@@ -0,0 +1,49 @@
+import {createEventHook, injectLocal, provideLocal} from "@vueuse/core";
+import {useProvidePaperBatchCondition} from "@/views/dz/papers/components/conditions/usePaperBatchCondition.js";
+import {getPaperExamTypes, getPaperSubjects} from "@/api/dz/papers.js";
+
+const key = Symbol('PaperFullCondition')
+export const useProvidePaperFullCondition = function (type) {
+    const {buildType, batchId, batchList} = useProvidePaperBatchCondition(type)
+
+    const examType = ref('')
+    const examTypes = ref([])
+
+    const subjectId = ref('')
+    const subjectList = ref([])
+
+    const readyEvent = createEventHook()
+
+    watch(batchId, async (batchId) => {
+        examType.value = ''
+        examTypes.value = []
+
+        const res = await getPaperExamTypes({buildType, batchId})
+        examTypes.value = res.data
+    })
+    watch([batchId, examType], async ([batchId, examType]) => {
+        subjectId.value = ''
+        subjectList.value = []
+
+        if(!batchId || !examType) return
+        const res = await getPaperSubjects({buildType, batchId, examType})
+        subjectList.value = res.data
+    })
+    watch([batchId, examType, subjectId], async ([batchId, examType, subjectId]) => {
+        if (!subjectId) return
+        const payload = {buildType, batchId, examType, subjectId}
+        await readyEvent.trigger(payload)
+    })
+
+    const payload = {buildType, batchId, batchList,
+        examType, examTypes,
+        subjectId, subjectList,
+        onFullReady: readyEvent.on}
+    provideLocal(key, payload)
+    return payload
+
+}
+
+export const useInjectPaperFullCondition = function (){
+    return injectLocal(key)
+}

+ 35 - 0
back-ui/src/views/dz/papers/components/conditions/usePaperKnowledgeCondition.js

@@ -0,0 +1,35 @@
+import {createEventHook, injectLocal, provideLocal} from "@vueuse/core";
+import {getPaperKnowledges} from "@/api/dz/papers.js";
+
+const key = Symbol('PaperKnowledgeCondition')
+export const useProvidePaperKnowledgeCondition = function () {
+    const knowledges = ref([])
+    const knowledgeNode = ref(null) // 单选节点
+    const knowledgeCheckNodes = ref([]) // 多选的节点
+    const knowledgeIds = computed(() => knowledgeCheckNodes.value.map(k => k.id))
+    const knowledgeId = computed(() => knowledgeNode.value?.id || '')
+    const knowledgeRemoveEvent = createEventHook()
+    const removeKnowledge = async (k) => {
+        const nodes = knowledgeCheckNodes.value
+        const idx = nodes.indexOf(k)
+        if (idx > -1) {
+            nodes.splice(idx, 1)
+            await knowledgeRemoveEvent.trigger(k)
+        }
+    }
+
+    const loadKnowledge = async function(payload) {
+        const res = await getPaperKnowledges(payload)
+        knowledges.value = res.data
+    }
+
+    const payload = {
+        knowledges, knowledgeNode, knowledgeId, knowledgeCheckNodes, knowledgeIds,
+        removeKnowledge, loadKnowledge, onKnowledgeRemove: knowledgeRemoveEvent.on
+    }
+    provideLocal(key, payload)
+    return payload
+}
+export const useInjectPaperKnowledgeCondition = function () {
+    return injectLocal(key)
+}

+ 111 - 0
back-ui/src/views/dz/papers/components/conditions/usePaperQuestionCondition.js

@@ -0,0 +1,111 @@
+import {useInjectPaperExactCondition} from "@/views/dz/papers/components/conditions/usePaperExactCondition.js";
+import {useInjectPaperFullCondition} from "@/views/dz/papers/components/conditions/usePaperFullCondition.js";
+import {useInjectPaperKnowledgeCondition} from "@/views/dz/papers/components/conditions/usePaperKnowledgeCondition.js";
+import {useInjectGlobalLoading} from "@/views/hooks/useGlobalLoading.js";
+import {ElMessage} from "element-plus";
+import {getPaperQuestions, getPaperQuestionTypes} from "@/api/dz/papers.js";
+import {injectLocal, provideLocal} from "@vueuse/core";
+
+const key = Symbol('PaperQuestionTypeCondition')
+
+export const useProvidePaperQuestionCondition = function (exactMode, allowMultiple, disableQuestionQuery) {
+    const keyword = ref('')
+    const qtpye = ref('') // 历史遗留拼写错误
+    const qTypes = ref([])
+
+    const pageNum = ref(1)
+    const pageSize = ref(10)
+    const total = ref(0)
+    const questionList = ref([])
+
+    const {subjectId, subjects} = exactMode ? useInjectPaperExactCondition() : useInjectPaperFullCondition()
+    const {knowledgeId, knowledgeIds} = useInjectPaperKnowledgeCondition()
+    const loading = useInjectGlobalLoading()
+
+    // question cart
+    const cart = ref([])
+    const currentSubject = computed(() => {
+        const demoId = cart.value[0]?.subjectId
+        return subjects.value.find(s => s.subjectId == demoId)
+    })
+    const groupedQuestions = computed(() => {
+        const results = {}
+        cart.value.forEach(q => {
+            if (!results[q.qtpye]) {
+                results[q.qtpye] = []
+            }
+            results[q.qtpye].push(q)
+        })
+        return Object.keys(results).map(k => ({qtpye: k, questions: results[k]}))
+    })
+    const hasQuestion = (q) => {
+        return cart.value.some(c => c.id == q.id)
+    }
+    const removeQuestion = (q) => {
+        const idx = cart.value.findIndex(c => c.id == q.id)
+        if (idx > -1) cart.value.splice(idx, 1)
+    }
+    const addQuestion = (q) => {
+        if (currentSubject.value && currentSubject.value.subjectId != q.subjectId) {
+            return ElMessage.error(`当前科目【${currentSubject.value.subjectName}】冲突`)
+        }
+        cart.value.push(q)
+    }
+    const removeQuestionGroup = (qtpye) => {
+        const ls = groupedQuestions.value.find(g => g.qtpye == qtpye)?.questions
+        ls?.forEach(q => removeQuestion(q))
+    }
+    const clearCart = () => cart.value = []
+
+    // hooks
+    watch([subjectId, knowledgeId, () => knowledgeIds.value.toString()], async ([subjectId, knowledgeId, knowledgeIds]) => {
+        // clean
+        qtpye.value = ''
+        qTypes.value = []
+
+        if (!subjectId) return
+        if (!knowledgeId && !knowledgeIds) return
+        const query = {subjectId, knowledgeIds: knowledgeId || knowledgeIds}
+        const res = await getPaperQuestionTypes(query)
+        qTypes.value = res.data
+    })
+
+    const questionQuery = computed(() => ({
+        pageNum: pageNum.value,
+        pageSize: pageSize.value,
+        title: keyword.value,
+        qtpye: qtpye.value,
+        knowledgeId: knowledgeId.value,
+        knowledges: knowledgeIds.value.toString()
+    }))
+    const getQuestionList = async function () {
+        if (disableQuestionQuery) return // 智能组卷时不需要查询
+        loading.value = true
+        try {
+            const res = await getPaperQuestions(questionQuery.value)
+            total.value = res.total
+            questionList.value = res.rows
+        } finally {
+            loading.value = false
+        }
+    }
+    watch([keyword, qtpye, knowledgeId, () => knowledgeIds.value.toString()], async ([title, qtpye, knowledgeId, knowledges]) => {
+        pageNum.value = 1
+        questionList.value = []
+        total.value = 0
+
+        if (!knowledgeId && !knowledges) return // 有知识点即可以查询题库
+        await getQuestionList(pageNum.value, pageSize.value)
+    })
+
+    const payload = {
+        keyword, qtpye, qTypes, pageNum, pageSize, total, questionList, getQuestionList,
+        cart, currentSubject, groupedQuestions,
+        hasQuestion, addQuestion, removeQuestion, removeQuestionGroup, clearCart
+    }
+    provideLocal(key, payload)
+    return payload
+}
+export const useInjectPaperQuestionCondition = function () {
+    return injectLocal(key)
+}

+ 10 - 0
back-ui/src/views/dz/papers/components/paper-exact-hand.vue

@@ -0,0 +1,10 @@
+<template>
+    定向手动组卷
+</template>
+
+<script setup name="PaperExactHand">
+</script>
+
+<style scoped>
+
+</style>

+ 69 - 0
back-ui/src/views/dz/papers/components/paper-exact-intelligent.vue

@@ -0,0 +1,69 @@
+<template>
+    <el-row :gutter="20">
+        <el-col :span="8">
+            <el-form label-width="68px">
+                <el-form-item label="批次">
+                    <el-select v-model="batchId" clearable style="width: 227px">
+                        <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
+                    </el-select>
+                </el-form-item>
+            </el-form>
+        </el-col>
+        <el-col :span="16">
+            <class-statistic-table exact-mode/>
+        </el-col>
+    </el-row>
+    <el-divider/>
+    <div class="text-center">
+        <el-button type="primary" size="large" @click="handleSubmit">生成试卷</el-button>
+    </div>
+</template>
+
+<script setup name="PaperExactIntelligent">
+import consts from "@/utils/consts.js";
+import {useProvidePaperBatchCondition} from "@/views/dz/papers/components/conditions/usePaperBatchCondition.js";
+import {
+    useProvidePaperClassStatisticCondition
+} from "@/views/dz/papers/components/conditions/usePaperClassStatisticCondition.js";
+import ClassStatisticTable from "@/views/dz/papers/components/plugs/class-statistic-table.vue";
+import {ElMessage} from "element-plus";
+import {buildPaperExactIntelligent} from "@/api/dz/papers.js";
+import {useInjectGlobalLoading} from "@/views/hooks/useGlobalLoading.js";
+
+const type = consts.enums.buildType.ExactIntelligent
+const {batchId, batchList, onBatchReady} = useProvidePaperBatchCondition(type)
+const {selectedClasses, classList, loadClassStatistic} = useProvidePaperClassStatisticCondition()
+const {loading} = useInjectGlobalLoading()
+
+const handleSubmit = async function () {
+    const classIds = selectedClasses.value.map(c => c.classId)
+    if (!batchId.value) return ElMessage.error('请选择批次')
+    if (!classIds.length) return ElMessage.error('请选择班级')
+
+    try {
+        loading.value = true
+        const commit = {buildType: type, batchId: batchId.value, classIds}
+        await buildPaperExactIntelligent(commit)
+        ElMessage.success('生成成功')
+        await _loadClassStatistic()
+    } finally {
+        loading.value = false
+    }
+}
+
+let statArgs = null // 保留参数,生成组卷后刷新用
+const _loadClassStatistic = async () => {
+    selectedClasses.value = []
+    classList.value = []
+    await loadClassStatistic(statArgs)
+}
+
+onBatchReady(async (payload) => {
+    statArgs = payload
+    await _loadClassStatistic()
+})
+</script>
+
+<style scoped>
+
+</style>

+ 10 - 0
back-ui/src/views/dz/papers/components/paper-full-hand.vue

@@ -0,0 +1,10 @@
+<template>
+    全量手动组卷
+</template>
+
+<script setup name="PaperFullHand">
+</script>
+
+<style scoped>
+
+</style>

+ 112 - 0
back-ui/src/views/dz/papers/components/paper-full-intelligent.vue

@@ -0,0 +1,112 @@
+<template>
+    <el-row :gutter="20">
+        <el-col :span="8">
+            <el-form label-width="68px">
+                <el-form-item label="批次">
+                    <el-select v-model="batchId" clearable style="width: 227px">
+                        <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="考生类型">
+                    <el-select v-model="examType" clearable style="width: 227px">
+                        <el-option v-for="e in examTypes" :label="e.dictLabel" :value="e.dictValue"/>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="科目">
+                    <el-select v-model="subjectId" clearable style="width: 227px">
+                        <el-option v-for="s in subjectList" :label="s.subjectName" :value="s.subjectId"/>
+                    </el-select>
+                </el-form-item>
+            </el-form>
+        </el-col>
+        <el-col :span="16">
+            <class-statistic-table />
+        </el-col>
+    </el-row>
+    <el-row :gutter="20" class="mt-5">
+        <el-col :span="6">
+            <knowledge-tree allow-multiple/>
+        </el-col>
+        <el-col :span="18">
+            <question-intelligent @submit="handleSubmit" />
+        </el-col>
+    </el-row>
+</template>
+
+<script setup name="PaperFullIntelligent">
+
+import consts from "@/utils/consts.js";
+import {useProvidePaperFullCondition} from "@/views/dz/papers/components/conditions/usePaperFullCondition.js";
+import ClassStatisticTable from "@/views/dz/papers/components/plugs/class-statistic-table.vue";
+import {
+    useProvidePaperClassStatisticCondition
+} from "@/views/dz/papers/components/conditions/usePaperClassStatisticCondition.js";
+import KnowledgeTree from "@/views/dz/papers/components/plugs/knowledge-tree.vue";
+import {useProvidePaperKnowledgeCondition} from "@/views/dz/papers/components/conditions/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 {sleep} from "@/utils/index.js";
+import router from "@/router/index.js";
+
+const type = consts.enums.buildType.FullIntelligent
+const {
+    batchId,
+    batchList,
+    examType,
+    examTypes,
+    subjectId,
+    subjectList,
+    onFullReady
+} = useProvidePaperFullCondition(type)
+const {selectedClasses, classList, loadClassStatistic} = useProvidePaperClassStatisticCondition()
+const {knowledgeNode, knowledgeCheckNodes, loadKnowledge} = useProvidePaperKnowledgeCondition()
+
+const handleSubmit = async (qTypes) => {
+    // validation
+    const classIds = selectedClasses.value.map(c => c.classId)
+    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('请填写题量')
+    if (!classIds.length) return ElMessage.error('请选择班级')
+
+    // build
+    const commit = {
+        buildType: type,
+        batchId: toValue(batchId),
+        examType: toValue(examType),
+        subjectId: toValue(subjectId),
+        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('生成成功,即将打开组卷记录')
+    await sleep(2000)
+    await router.push('/paper/list')
+}
+
+let statArg = null
+const _loadClassStatistic = async () => {
+    selectedClasses.value = []
+    classList.value = []
+
+    await loadClassStatistic(statArg)
+}
+onFullReady(async (payload) => {
+    statArg = payload
+    knowledgeNode.value = null
+    knowledgeCheckNodes.value = []
+
+    await loadKnowledge(payload)
+    await _loadClassStatistic()
+})
+</script>
+
+<style scoped>
+
+</style>

+ 48 - 0
back-ui/src/views/dz/papers/components/plugs/class-statistic-table.vue

@@ -0,0 +1,48 @@
+<template>
+    <Table :data="classList" :columns="columns" selection-mode="multiple" row-key="classId"
+           :selected-rows="selectedClasses" @selection-change="handleSelectionChange"/>
+</template>
+
+<script setup name="ClassStatisticTable">
+import {
+    useInjectPaperClassStatisticCondition
+} from "@/views/dz/papers/components/conditions/usePaperClassStatisticCondition.js";
+import Table from '@/components/Table/index.vue';
+
+const props = defineProps({
+    exactMode: Boolean
+})
+
+const {classList, selectedClasses} = useInjectPaperClassStatisticCondition()
+
+const ids = ref([]);
+const single = ref(true);
+const multiple = ref(true);
+
+const columns = props.exactMode ? [
+    {label: '班级名称', prop: 'className'},
+    {label: '总人数', prop: 'total'},
+    {label: '组卷已完成', prop: 'send'},
+    {label: '组卷未完成', prop: 'unfinish'},
+    {label: '定向未组卷', prop: 'unsend'},
+    {label: '未定向未组卷', prop: 'unexact'},
+] : [
+    {label: '班级名称', prop: 'className'},
+    {label: '总人数', prop: 'total'},
+    {label: '组卷已完成', prop: 'send'},
+    {label: '组卷未完成', prop: 'unfinish'},
+    {label: '未组卷', prop: 'unsend'},
+]
+
+function handleSelectionChange(selection) {
+    ids.value = selection.map((item) => item.classId);
+    single.value = selection.length != 1;
+    multiple.value = !selection.length;
+
+    selectedClasses.value = selection
+}
+</script>
+
+<style scoped>
+
+</style>

+ 42 - 0
back-ui/src/views/dz/papers/components/plugs/knowledge-tree.vue

@@ -0,0 +1,42 @@
+<template>
+    <el-input v-model="keyword" placeholder="请输入知识点名称" clearable prefix-icon="Search" style="margin-bottom: 20px"/>
+    <el-tree ref="treeRef" v-if="knowledges.length" :data="knowledges" :props="{ label: 'name', children: 'children' }"
+             :expand-on-click-node="false" :filter-node-method="filterNode" node-key="id" highlight-current
+             default-expand-all :show-checkbox="allowMultiple" @node-click="handleNodeClick" @check="handleNodeCheck"/>
+</template>
+
+<script setup name="KnowledgeTree">
+import {useInjectPaperKnowledgeCondition} from "@/views/dz/papers/components/conditions/usePaperKnowledgeCondition.js";
+
+const props = defineProps({
+    allowMultiple: Boolean
+})
+
+const treeRef = ref(null)
+const keyword = ref('')
+const {knowledges, knowledgeNode, knowledgeCheckNodes, onKnowledgeRemove} = useInjectPaperKnowledgeCondition()
+const filterNode = function (value, data) {
+    if (!value) return true
+    return data.name.indexOf(value) !== -1
+}
+
+const handleNodeClick = function (data) {
+    knowledgeNode.value = data
+}
+
+const handleNodeCheck = function (data, {checkedNodes}) {
+    // 多选时,只保留叶子节点
+    knowledgeCheckNodes.value = checkedNodes.filter(k => !!!k.children?.length)
+}
+
+watch(keyword, val => {
+    treeRef.value.filter(val)
+})
+onKnowledgeRemove((k) => {
+    treeRef.value.setChecked(k, false, false)
+})
+</script>
+
+<style scoped>
+
+</style>

+ 45 - 0
back-ui/src/views/dz/papers/components/plugs/question-intelligent.vue

@@ -0,0 +1,45 @@
+<template>
+    <div class="text-main mb-3 flex items-center">
+        <div :class="seqClass">1</div>选择知识点(从左侧勾选)
+    </div>
+    <div class="flex flex-row flex-wrap gap-3">
+        <el-tag v-for="k in knowledgeCheckNodes" type="primary" round closable @close="removeKnowledge(k)">{{k.name}}</el-tag>
+    </div>
+    <div class="text-main mt-10 mb-3 flex items-center">
+        <div :class="seqClass">2</div>试题设置
+    </div>
+    <div class="flex flex-row flex-warp gap-10">
+        <div v-for="t in qTypes" class="flex items-center gap-3">
+            <el-text class="font-bold break-keep">{{t.dictLabel}}</el-text>
+            <el-input-number v-model="t.count" :min="0"/>
+        </div>
+    </div>
+    <el-divider />
+    <div class="text-center">
+        <el-button type="primary" size="large" @click="buildPaper">生成试卷</el-button>
+    </div>
+</template>
+
+<script setup name="QuestionIntelligent">
+
+import {useProvidePaperQuestionCondition} from "@/views/dz/papers/components/conditions/usePaperQuestionCondition.js";
+import {useInjectPaperKnowledgeCondition} from "@/views/dz/papers/components/conditions/usePaperKnowledgeCondition.js";
+
+const props = defineProps({
+    allowMultiple: Boolean,
+    exactMode: Boolean
+})
+const emits = defineEmits(['submit'])
+
+const seqClass = 'inline-block rounded-full bg-blue-100 w-5 h-5 text-center mr-2'
+const {knowledgeCheckNodes, removeKnowledge} = useInjectPaperKnowledgeCondition()
+const {qTypes} = useProvidePaperQuestionCondition(props.exactMode, props.allowMultiple, true)
+
+const buildPaper = function () {
+    emits('submit', qTypes)
+}
+</script>
+
+<style scoped>
+
+</style>

+ 1 - 1
back-ui/src/views/dz/papers/hooks/usePaperQuestionCondition.js

@@ -1,7 +1,7 @@
 import {useInjectPaperExactCondition} from "@/views/dz/papers/hooks/usePaperExactCondition.js";
 import {useInjectPaperFullCondition} from "@/views/dz/papers/hooks/usePaperFullCondition.js";
 import {injectLocal, provideLocal} from "@vueuse/core";
-import {useInjectLoading} from "@/views/hooks/useLoading.js";
+import {useInjectLoading} from "@/views/hooks/useGlobalLoading.js";
 import {getPaperQuestions, getPaperQuestionTypes} from "@/api/dz/papers.js";
 import {ElMessage} from "element-plus";
 

+ 37 - 13
back-ui/src/views/dz/papers/index.vue

@@ -1,26 +1,50 @@
 <template>
     <div class="app-container" v-loading="loading">
-        <el-tabs @tab-change="handleTabChange">
-            <el-tab-pane label="手动组卷">
-                <paper-by-hand />
-            </el-tab-pane>
-            <el-tab-pane label="智能组卷">
-                <paper-by-intelligent v-if="intelligentVisited" />
+        <el-tabs v-model="currentTab">
+            <el-tab-pane v-for="t in tabs" :label="t.label" :name="t.name">
+                <component :is="t.page" v-if="t.visited"/>
             </el-tab-pane>
         </el-tabs>
     </div>
 </template>
 
 <script setup name="PaperIndex">
-import PaperByHand from "@/views/dz/papers/components/paper-by-hand.vue";
-import PaperByIntelligent from "@/views/dz/papers/components/paper-by-intelligent.vue";
-import {useProvideLoading} from "@/views/hooks/useLoading.js";
+import {useProvideGlobalLoading} from "@/views/hooks/useGlobalLoading.js";
+import PaperExactIntelligent from "@/views/dz/papers/components/paper-exact-intelligent.vue";
+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";
+
+const {loading} = useProvideGlobalLoading()
 
-const {loading} = useProvideLoading()
+const tabs = ref([{
+    name: 'ExactIntelligent',
+    label: '定向智能',
+    page: markRaw(PaperExactIntelligent) ,
+    visited: true
+}, {
+    name: 'FullIntelligent',
+    label: '全量智能',
+    page: markRaw(PaperFullIntelligent),
+    visited: false
+}, {
+    name: 'ExactHand',
+    label: '定向手动',
+    page: markRaw(PaperExactHand),
+    visited: false
+}, {
+    name: 'FullHand',
+    label: '全量手动',
+    page: markRaw(PaperFullHand),
+    visited: false
+}])
+const currentTab = ref('ExactIntelligent')
 
-// 延迟渲染,减少初次打开页面时的加载项
-const intelligentVisited = ref(false)
-const handleTabChange = () => intelligentVisited.value = true
+watch(currentTab, tabName => {
+    // 通过visited=true 延迟渲染
+    const tab = tabs.value.find(t => t.name == tabName)
+    if (tab) tab.visited = true
+})
 </script>
 
 <style scoped></style>

+ 249 - 2
back-ui/src/views/dz/papers/list.vue

@@ -1,7 +1,254 @@
 <template>
-  <div>组卷历史</div>
+    <div class="app-container">
+        <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+            <el-form-item label="试卷名称" prop="name">
+                <el-input v-model="queryParams.name" placeholder="请输入试卷名称" clearable @keyup.enter="handleQuery"/>
+            </el-form-item>
+            <el-form-item label="科目" prop="subjectId">
+                <el-select v-model="queryParams.subjectId" clearable placeholder="请选择科目" style="width: 170px;">
+                    <el-option v-for="s in subjectList" :label="s.subjectName" :value="s.subjectId"/>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="组卷类型" prop="buildType">
+                <el-select v-model="queryParams.buildType" clearable placeholder="请选择组卷类型" style="width: 170px;">
+                    <!--        TODO: 组卷类型        -->
+                </el-select>
+            </el-form-item>
+            <el-form-item label="试卷批次" prop="batchId">
+                <el-select v-model="queryParams.batchId" clearable placeholder="请选择批次" style="width: 170px;">
+                    <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="考生类型" prop="examType">
+                <el-select v-model="queryParams.examType" placeholder="请选择考生类型" clearable style="width: 170px;">
+                    <el-option v-for="dict in exam_type" :key="dict.value" :label="dict.label" :value="dict.value"/>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="发送情况" prop="state">
+                <el-select v-model="queryParams.state" placeholder="请选择考生类型" clearable style="width: 170px;">
+                    <!--         TODO: 发送情况           -->
+                </el-select>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-row :gutter="10" class="mb8">
+            <!--            <el-col :span="1.5">-->
+            <!--                <el-button-->
+            <!--                    type="primary"-->
+            <!--                    plain-->
+            <!--                    icon="Plus"-->
+            <!--                    @click="handleAdd"-->
+            <!--                    v-hasPermi="['dz:subject:add']"-->
+            <!--                >新增</el-button>-->
+            <!--            </el-col>-->
+            <!--            <el-col :span="1.5">-->
+            <!--                <el-button-->
+            <!--                    type="success"-->
+            <!--                    plain-->
+            <!--                    icon="Edit"-->
+            <!--                    :disabled="single"-->
+            <!--                    @click="handleUpdate"-->
+            <!--                    v-hasPermi="['dz:subject:edit']"-->
+            <!--                >修改</el-button>-->
+            <!--            </el-col>-->
+            <!--            <el-col :span="1.5">-->
+            <!--                <el-button-->
+            <!--                    type="danger"-->
+            <!--                    plain-->
+            <!--                    icon="Delete"-->
+            <!--                    :disabled="multiple"-->
+            <!--                    @click="handleDelete"-->
+            <!--                    v-hasPermi="['dz:subject:remove']"-->
+            <!--                >删除</el-button>-->
+            <!--            </el-col>-->
+            <!--            <el-col :span="1.5">-->
+            <!--                <el-button-->
+            <!--                    type="warning"-->
+            <!--                    plain-->
+            <!--                    icon="Download"-->
+            <!--                    @click="handleExport"-->
+            <!--                    v-hasPermi="['dz:subject:export']"-->
+            <!--                >导出</el-button>-->
+            <!--            </el-col>-->
+            <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+
+        <el-table v-loading="loading" :data="paperList" @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" align="center"/>
+            <el-table-column label="试卷ID" align="center" prop="id" width="80"/>
+            <el-table-column label="试卷名称" align="center" prop="paperName"/>
+            <el-table-column label="科目" align="center" prop="subjectName"/>
+            <el-table-column label="试卷批次" align="center" prop="batchName"/>
+            <el-table-column label="组卷类型" align="center" prop="buildType"/>
+            <el-table-column label="状态" align="center" prop="state"/>
+            <el-table-column label="创建人" align="center" prop="createBy"/>
+            <el-table-column label="试卷分类" align="center" prop="paperType"/>
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+                <template #default="scope">
+                    <el-button link type="primary" icon="Edit" @click="handleEdit(scope.row)"
+                               v-hasPermi="['dz:paper:edit']">编辑
+                    </el-button>
+                    <el-button link type="primary" icon="Download" @click="handleDownload(scope.row)"
+                               v-hasPermi="['dz:paper:download']">下载
+                    </el-button>
+                    <el-button link type="primary" icon="Finished" @click="handlePublish(scope.row)"
+                               v-hasPermi="['dz:paper:download']">发布试卷
+                    </el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <pagination v-show="total>0" :total="total" v-model:page="queryParams.pageNum"
+                    v-model:limit="queryParams.pageSize" @pagination="getList"/>
+
+        <!-- 添加或修改科目对话框 -->
+        <el-dialog :title="publishDialogTitle" v-model="openPublish" width="500px" append-to-body>
+            <paper-publish :paper="openPaper" />
+            <template #footer>
+                <div class="dialog-footer">
+                    <el-button type="primary" @click="handlePublishConfirm">确 定</el-button>
+                    <el-button @click="cancel">取 消</el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
 </template>
 
-<script setup></script>
+<script setup name="PaperList">
+
+import {getPaperBatches, getPaperList, getPaperSubjects} from "@/api/dz/papers.js";
+import PaperPublish from "@/views/dz/papers/components/paper-publish.vue";
+
+const {proxy} = getCurrentInstance()
+const {exam_type} = proxy.useDict('build_type', 'exam_type')
+
+const paperList = ref([])
+const open = ref(false)
+const loading = ref(false)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+
+const subjectList = ref([])
+const batchList = ref([])
+const subjectMap = computed(() => {
+    const obj = {}
+    subjectList.value.forEach(s => obj[s.subjectId] = s.subjectName)
+    return obj
+})
+const batchMap = computed(() => {
+    const obj = {}
+    batchList.value.forEach(b => obj[b.batchId] = b.name)
+    return obj
+})
+
+const openPublish = ref(false)
+const openPaper = ref(null)
+const publishDialogTitle = computed(() => `发布试卷:${openPaper?.paperName}`)
+
+const data = reactive({
+    form: {},
+    queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        subjectId: null,
+        buildType: null,
+        batchId: null,
+        state: null,
+        examType: null
+    },
+    rules: {}
+})
+
+const {queryParams, form, rules} = toRefs(data)
+
+function getList() {
+    loading.value = true
+
+    getPaperList(queryParams.value).then(response => {
+        response.rows.forEach(p => {
+            p.subjectName = subjectMap.value[p.subjectId] || ''
+            p.batchName = batchMap.value[p.batchId] || ''
+        })
+        paperList.value = response.rows
+        total.value = response.total
+        loading.value = false
+    })
+}
+
+// 取消按钮
+function cancel() {
+    open.value = false
+    reset()
+}
+
+// 表单重置
+function reset() {
+    form.value = {
+        subjectId: null,
+        subjectName: null,
+        pinyin: null,
+        sort: null,
+        locations: null,
+        examTypes: null
+    }
+    proxy.resetForm("subjectRef")
+}
+
+/** 搜索按钮操作 */
+function handleQuery() {
+    queryParams.value.pageNum = 1
+    getList()
+}
+
+/** 重置按钮操作 */
+function resetQuery() {
+    proxy.resetForm("queryRef")
+    handleQuery()
+}
+
+// 多选框选中数据
+function handleSelectionChange(selection) {
+    ids.value = selection.map(item => item.subjectId)
+    single.value = selection.length != 1
+    multiple.value = !selection.length
+}
+
+// operations
+const handleEdit = function (paper) {
+
+}
+
+const handleDownload = function (paper) {
+    proxy.download('/learn/paper/export', {
+        paperId: paper.id
+    }, `${paper.paperName}_${new Date().getTime()}.xlsx`)
+}
+
+const handlePublish = function (paper) {
+    openPublish.value = true
+    openPaper.value = paper
+}
+
+const handlePublishConfirm = function () {
+    openPublish.value = false
+}
+
+// hooks
+onMounted(async () => {
+    const resSubject = await getPaperSubjects()
+    subjectList.value = resSubject.data
+    const resBatch = await getPaperBatches()
+    batchList.value = resBatch.data
+    getList()
+})
+</script>
 
 <style lang="scss" scoped></style>

+ 2 - 2
back-ui/src/views/hooks/useLoading.js → back-ui/src/views/hooks/useGlobalLoading.js

@@ -2,13 +2,13 @@ import {injectLocal, provideLocal} from "@vueuse/core";
 
 const key = Symbol('GlobalLoading')
 
-export const useProvideLoading = function () {
+export const useProvideGlobalLoading = function () {
     const loading = ref(false)
     const payload = {loading}
     provideLocal(key, payload)
     return payload
 }
 
-export const useInjectLoading = function () {
+export const useInjectGlobalLoading = function () {
     return injectLocal(key)
 }