Sfoglia il codice sorgente

Merge branch 'master' of http://49.234.186.218:9000/root/ieplus

month-red-love 1 mese fa
parent
commit
6a1b1c9957

+ 150 - 137
back-ui/src/router/index.js

@@ -1,4 +1,4 @@
-import { createWebHistory, createRouter } from 'vue-router'
+import {createWebHistory, createRouter} from 'vue-router'
 /* Layout */
 import Layout from '@/layout'
 
@@ -26,149 +26,162 @@ import Layout from '@/layout'
 
 // 公共路由
 export const constantRoutes = [
-  {
-    path: '/redirect',
-    component: Layout,
-    hidden: true,
-    children: [
-      {
-        path: '/redirect/:path(.*)',
-        component: () => import('@/views/redirect/index.vue')
-      }
-    ]
-  },
-  {
-    path: '/login',
-    component: () => import('@/views/login'),
-    hidden: true
-  },
-  {
-    path: '/register',
-    component: () => import('@/views/register'),
-    hidden: true
-  },
-  {
-    path: "/:pathMatch(.*)*",
-    component: () => import('@/views/error/404'),
-    hidden: true
-  },
-  {
-    path: '/401',
-    component: () => import('@/views/error/401'),
-    hidden: true
-  },
-  {
-    path: '',
-    component: Layout,
-    redirect: '/index',
-    children: [
-      {
-        path: '/index',
-        component: () => import('@/views/index'),
-        name: 'Index',
-        meta: { title: '首页', icon: 'dashboard', affix: true }
-      }
-    ]
-  },
-  {
-    path: '/user',
-    component: Layout,
-    hidden: true,
-    redirect: 'noredirect',
-    children: [
-      {
-        path: 'profile/:activeTab?',
-        component: () => import('@/views/system/user/profile/index'),
-        name: 'Profile',
-        meta: { title: '个人中心', icon: 'user' }
-      }
-    ]
-  }
+    {
+        path: '/redirect',
+        component: Layout,
+        hidden: true,
+        children: [
+            {
+                path: '/redirect/:path(.*)',
+                component: () => import('@/views/redirect/index.vue')
+            }
+        ]
+    },
+    {
+        path: '/login',
+        component: () => import('@/views/login'),
+        hidden: true
+    },
+    {
+        path: '/register',
+        component: () => import('@/views/register'),
+        hidden: true
+    },
+    {
+        path: "/:pathMatch(.*)*",
+        component: () => import('@/views/error/404'),
+        hidden: true
+    },
+    {
+        path: '/401',
+        component: () => import('@/views/error/401'),
+        hidden: true
+    },
+    {
+        path: '',
+        component: Layout,
+        redirect: '/index',
+        children: [
+            {
+                path: '/index',
+                component: () => import('@/views/index'),
+                name: 'Index',
+                meta: {title: '首页', icon: 'dashboard', affix: true}
+            }
+        ]
+    },
+    {
+        path: '/paper',
+        component: Layout,
+        hidden: true,
+        children: [
+            {
+                path: 'detail',
+                name: 'PaperDetail',
+                component: () => import('@/views/dz/papers/detail.vue'),
+                meta: {title: '试卷详情'}
+            }
+        ]
+    },
+    {
+        path: '/user',
+        component: Layout,
+        hidden: true,
+        redirect: 'noredirect',
+        children: [
+            {
+                path: 'profile/:activeTab?',
+                component: () => import('@/views/system/user/profile/index'),
+                name: 'Profile',
+                meta: {title: '个人中心', icon: 'user'}
+            }
+        ]
+    }
 ]
 
 // 动态路由,基于用户权限动态去加载
 export const dynamicRoutes = [
-  {
-    path: '/system/user-auth',
-    component: Layout,
-    hidden: true,
-    permissions: ['system:user:edit'],
-    children: [
-      {
-        path: 'role/:userId(\\d+)',
-        component: () => import('@/views/system/user/authRole'),
-        name: 'AuthRole',
-        meta: { title: '分配角色', activeMenu: '/system/user' }
-      }
-    ]
-  },
-  {
-    path: '/system/role-auth',
-    component: Layout,
-    hidden: true,
-    permissions: ['system:role:edit'],
-    children: [
-      {
-        path: 'user/:roleId(\\d+)',
-        component: () => import('@/views/system/role/authUser'),
-        name: 'AuthUser',
-        meta: { title: '分配用户', activeMenu: '/system/role' }
-      }
-    ]
-  },
-  {
-    path: '/system/dict-data',
-    component: Layout,
-    hidden: true,
-    permissions: ['system:dict:list'],
-    children: [
-      {
-        path: 'index/:dictId(\\d+)',
-        component: () => import('@/views/system/dict/data'),
-        name: 'Data',
-        meta: { title: '字典数据', activeMenu: '/system/dict' }
-      }
-    ]
-  },
-  {
-    path: '/monitor/job-log',
-    component: Layout,
-    hidden: true,
-    permissions: ['monitor:job:list'],
-    children: [
-      {
-        path: 'index/:jobId(\\d+)',
-        component: () => import('@/views/monitor/job/log'),
-        name: 'JobLog',
-        meta: { title: '调度日志', activeMenu: '/monitor/job' }
-      }
-    ]
-  },
-  {
-    path: '/tool/gen-edit',
-    component: Layout,
-    hidden: true,
-    permissions: ['tool:gen:edit'],
-    children: [
-      {
-        path: 'index/:tableId(\\d+)',
-        component: () => import('@/views/tool/gen/editTable'),
-        name: 'GenEdit',
-        meta: { title: '修改生成配置', activeMenu: '/tool/gen' }
-      }
-    ]
-  }
+    {
+        path: '/system/user-auth',
+        component: Layout,
+        hidden: true,
+        permissions: ['system:user:edit'],
+        children: [
+            {
+                path: 'role/:userId(\\d+)',
+                component: () => import('@/views/system/user/authRole'),
+                name: 'AuthRole',
+                meta: {title: '分配角色', activeMenu: '/system/user'}
+            }
+        ]
+    },
+    {
+        path: '/system/role-auth',
+        component: Layout,
+        hidden: true,
+        permissions: ['system:role:edit'],
+        children: [
+            {
+                path: 'user/:roleId(\\d+)',
+                component: () => import('@/views/system/role/authUser'),
+                name: 'AuthUser',
+                meta: {title: '分配用户', activeMenu: '/system/role'}
+            }
+        ]
+    },
+    {
+        path: '/system/dict-data',
+        component: Layout,
+        hidden: true,
+        permissions: ['system:dict:list'],
+        children: [
+            {
+                path: 'index/:dictId(\\d+)',
+                component: () => import('@/views/system/dict/data'),
+                name: 'Data',
+                meta: {title: '字典数据', activeMenu: '/system/dict'}
+            }
+        ]
+    },
+    {
+        path: '/monitor/job-log',
+        component: Layout,
+        hidden: true,
+        permissions: ['monitor:job:list'],
+        children: [
+            {
+                path: 'index/:jobId(\\d+)',
+                component: () => import('@/views/monitor/job/log'),
+                name: 'JobLog',
+                meta: {title: '调度日志', activeMenu: '/monitor/job'}
+            }
+        ]
+    },
+    {
+        path: '/tool/gen-edit',
+        component: Layout,
+        hidden: true,
+        permissions: ['tool:gen:edit'],
+        children: [
+            {
+                path: 'index/:tableId(\\d+)',
+                component: () => import('@/views/tool/gen/editTable'),
+                name: 'GenEdit',
+                meta: {title: '修改生成配置', activeMenu: '/tool/gen'}
+            }
+        ]
+    }
 ]
 
 const router = createRouter({
-  history: createWebHistory('/admin'),
-  routes: constantRoutes,
-  scrollBehavior(to, from, savedPosition) {
-    if (savedPosition) {
-      return savedPosition
-    }
-    return { top: 0 }
-  },
+    history: createWebHistory('/admin'),
+    routes: constantRoutes,
+    scrollBehavior(to, from, savedPosition) {
+        if (savedPosition) {
+            return savedPosition
+        }
+        return {top: 0}
+    },
 })
 
 export default router

+ 6 - 1
back-ui/src/utils/index.js

@@ -387,4 +387,9 @@ export function camelCase(str) {
 export function isNumberStr(str) {
   return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
 }
- 
+
+export function sleep(delay) {
+  return new Promise((resolve) => {
+    setTimeout(() => resolve(), delay)
+  })
+}

+ 20 - 7
back-ui/src/views/dz/papers/components/paper-question-hand.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="text-main mb-3">
-        当前查询知识点(从左侧选择):<el-text type="primary font-bold">{{ knowledgeNode?.name }}</el-text>
+        当前查询知识点(从左侧选择):<el-text size="large" type="primary" class="font-bold">{{ knowledgeNode?.name }}</el-text>
     </div>
     <div class="flex flex-row items-center gap-3">
         <el-input v-model="keywordLocal" clearable prefix-icon="search" placeholder="输入题目关键字-回车触发搜索"
@@ -19,12 +19,11 @@
             <el-divider style="margin: 10px 0"/>
             <div class="flex items-center justify-between">
                 <el-link type="danger" plain icon="delete" @click="clearCart">全部清除</el-link>
-                <el-button type="primary">生成试卷</el-button>
+                <el-button type="primary" @click="buildPaper">生成试卷</el-button>
             </div>
             <template #reference>
-                <el-button type="primary" size="large" icon="shopping-cart" class="ml-auto">试题篮({{
-                        cart.length
-                    }})
+                <el-button type="primary" size="large" icon="shopping-cart" class="ml-auto">
+                    试题篮({{ cart.length }})
                 </el-button>
             </template>
         </el-popover>
@@ -56,10 +55,12 @@
 </template>
 
 <script setup name="PaperQuestionHand">
-
+import router from '@/router'
+import {ElMessage} from "element-plus";
 import {useProvidePaperQuestionCondition} from "@/views/dz/papers/hooks/usePaperQuestionCondition.js";
 import {useInjectPaperExactCondition} from "@/views/dz/papers/hooks/usePaperExactCondition.js";
 import {useInjectPaperFullCondition} from "@/views/dz/papers/hooks/usePaperFullCondition.js";
+import {usePaperStorage} from "@/views/dz/papers/hooks/usePaperStorage.js";
 import QuestionContent from "@/views/components/question-content.vue";
 
 const props = defineProps({
@@ -70,7 +71,7 @@ const props = defineProps({
 const showParse = ref(false)
 const showParseQuestion = ref(null)
 
-const {knowledgeNode} = props.exactMode ? useInjectPaperExactCondition() : useInjectPaperFullCondition()
+const {knowledgeNode, paperArgs} = props.exactMode ? useInjectPaperExactCondition() : useInjectPaperFullCondition()
 const {
     keyword,
     qtpye,
@@ -92,6 +93,18 @@ const {
 
 const keywordLocal = ref('')
 const confirmKeyword = (val) => keyword.value = keywordLocal.value
+
+const buildPaper = function () {
+    // validation
+    const {batchId} = paperArgs.value
+    if (!batchId) return ElMessage.error('请选择批次')
+    if (!knowledgeNode.value) return ElMessage.error('请选择知识点')
+    if (!cart.value.length) return ElMessage.error('请将试题加入试题篮')
+
+    const paper = {...paperArgs.value, questions: cart.value}
+    usePaperStorage(paper)
+    router.push({name: 'PaperDetail'})
+}
 </script>
 
 <style scoped>

+ 33 - 2
back-ui/src/views/dz/papers/components/paper-question-intelligent.vue

@@ -15,7 +15,7 @@
         </div>
     </div>
     <div class="mt-10 text-right">
-        <el-button type="primary" size="large">生成试卷</el-button>
+        <el-button type="primary" size="large" @click="buildPaper">生成试卷</el-button>
     </div>
 </template>
 
@@ -24,6 +24,11 @@
 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 {ElMessage} from "element-plus";
+import {buildPaperAuto} from "@/api/dz/papers.js";
+import {sleep} from "@/utils/index.js";
+import router from "@/router/index.js";
 
 const props = defineProps({
     allowMultiple: Boolean,
@@ -31,8 +36,34 @@ const props = defineProps({
 })
 
 const seqClass = 'inline-block rounded-full bg-blue-100 w-5 h-5 text-center mr-2'
-const {knowledgeCheckNodes, removeKnowledge} = props.exactMode ? useInjectPaperExactCondition() : useInjectPaperFullCondition()
+const {knowledgeCheckNodes, removeKnowledge, paperArgs} = props.exactMode ? useInjectPaperExactCondition() : useInjectPaperFullCondition()
 const {qTypes} = useProvidePaperQuestionCondition(props.exactMode, props.allowMultiple, true)
+const {loading} = useInjectLoading()
+
+const buildPaper = async function () {
+    // validation
+    const {batchId} = paperArgs.value
+    if (!batchId) return ElMessage.error('请选择批次')
+    if (!knowledgeCheckNodes.value.length) return ElMessage.error('请选择知识点')
+    if (!qTypes.value.length || qTypes.value.every(t => !t.count)) return ElMessage.error('请填写题量')
+
+    // build
+    const commit = {
+        ...paperArgs.value,
+        paperDef: {
+            knowIds: knowledgeCheckNodes.value.map(k => k.id),
+            types: qTypes.value.map(t => ({
+                type: t.dictValue,
+                title: t.dictLabel,
+                count: t.count
+            }))
+        }
+    }
+    await buildPaperAuto(commit)
+    ElMessage.success('生成成功,即将打开组卷记录')
+    sleep(2000)
+    router.push('/paper/list')
+}
 </script>
 
 <style scoped>

+ 278 - 0
back-ui/src/views/dz/papers/detail.vue

@@ -1,8 +1,286 @@
 <template>
+    <div class="app-container">
+        <div class="flex flex-row justify-between items-center">
+            <el-button icon="back" @click="router.back()">返回</el-button>
+            <div class="flex-1 flex justify-center items-center">
+                <el-text>{{ paperLocal.batchName }}</el-text>
+                <el-input v-model="paperLocal.name" placeholder="输入试卷名称" class="ml-5" style="width: 220px"/>
+            </div>
+            <el-button type="primary" icon="select" @click="handleSavePaper">保存</el-button>
+        </div>
+        <el-divider/>
+        <el-card v-for="(group, idx) in groupedQuestions" shadow="never" class="mb-5">
+            <div :class="'drag_' + idx">
+                <div slot="header" class="clearfix" @mouseover="showScore(group, idx)"
+                     @mouseout="hideScore(group, idx)" draggable="true"
+                     @dragstart="handleDragStart($event, group)"
+                     @dragover.prevent="handleDragOver($event, group)"
+                     @drop="handleDragEnter($event, group)" @dragend="handleDragEnd($event, group)">
+                    <el-text class="font-bold">
+                        {{ numberToChinese(idx + 1) }}、{{ group.title }}(共{{ group.num }}题;共{{ group.score }}分)
+                    </el-text>
+                    <span style="float: right; display: none" :id="'parent_score_' + idx">
+            <el-button type="text" @click="openGroupScore=true,groupIdx=idx">批量设置得分</el-button>
+            <el-button type="text" @click="deleteType(idx)">删除分类</el-button>
+          </span>
+                </div>
+                <div v-for="(q, i) in group.list" class="mb-5"
+                     @mouseover="showQuestionScore(idx, i)" @mouseout="hideQuestionScore(idx, i)"
+                     @dragstart="handleQuestionDragStart($event, q, i)"
+                     @dragover.prevent="handleQuestionDragOver($event, q)"
+                     @drop="handleQuestionDragEnter($event, q, idx, i)"
+                     @dragend="handleQuestionDragEnd($event, q)" draggable="true">
+                    <div style="display: none" :id="'score_' + idx + '_' + i">
+                        <el-button type="text" @click="openScore=true,groupIdx=idx,qIdx=i">
+                            设置得分
+                        </el-button>
+                        <el-button type="text" @click="showQuestionParse(q)">解析</el-button>
+                        <el-button type="text" @click="deleteQuestion(idx, i)">删除题目</el-button>
+                    </div>
+                    <div>
+                        {{ i + 1 }}.
+                        <span style="color: #1890ff">题号:{{ q.id }}({{ q.score }}分)</span><span
+                        v-html="q.title"></span>
+                    </div>
+                </div>
+            </div>
+        </el-card>
 
+        <!-- 批量设置分数-->
+        <el-dialog v-model="openGroupScore" width="500px" append-to-body>
+            <el-form label-width="80px">
+                <el-form-item label="分数" prop="parentScore">
+                    <el-input-number v-model.number="groupScore" :min="1" placeholder="请输入分数"/>
+                </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="setGroupScore">确 定</el-button>
+                <el-button @click="openGroupScore=false">取 消</el-button>
+            </div>
+        </el-dialog>
+        <!-- 设置题目分数 -->
+        <el-dialog v-model="openScore" width="500px" append-to-body>
+            <el-form label-width="80px">
+                <el-form-item label="分数" prop="parentScore">
+                    <el-input-number v-model.number="qScore" :min="1" placeholder="请输入分数"/>
+                </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="setScore">确 定</el-button>
+                <el-button @click="openScore=false">取 消</el-button>
+            </div>
+        </el-dialog>
+        <!-- 设置题目解析 -->
+        <el-dialog v-model="openParse" title="解析" width="70%" append-to-body center>
+            <el-form label-width="80px" id="paperDialog">
+                <el-form-item label="解析">
+                    <editor v-model="parseCopy.parse" :min-height="122"/>
+                </el-form-item>
+                <el-form-item label="答案1">
+                    <editor v-model="parseCopy.answer1" :min-height="122"/>
+                </el-form-item>
+                <el-form-item label="答案2">
+                    <editor v-model="parseCopy.answer2" :min-height="122"/>
+                </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="setQuestionParse">确 定</el-button>
+                <el-button @click="openParse=false">取 消</el-button>
+            </div>
+        </el-dialog>
+    </div>
 </template>
 
 <script setup name="PaperDetail">
+
+import {usePaperResolver} from "@/views/dz/papers/hooks/usePaperStorage.js";
+import router from "@/router/index.js";
+import {ElMessage} from "element-plus";
+import {buildPaperManual} from "@/api/dz/papers.js";
+
+const {paperLocal, groupedQuestions, toCommitPaper} = usePaperResolver()
+
+const chnNumChar = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
+const chnUnitSection = ["", "万", "亿", "万亿", "亿亿"];
+const chnUnitChar = ["", "十", "百", "千"];
+
+const groupIdx = ref(0)
+const qIdx = ref(0)
+const openGroupScore = ref(false)
+const openScore = ref(false)
+const groupScore = ref(1)
+const qScore = ref(1)
+const openParse = ref(false)
+const parseQuestion = ref(null)
+const parseCopy = ref(null)
+
+const sectionToChinese = function (section) {
+    let strIns = "", chnStr = "";
+    let unitPos = 0;
+    let zero = true;
+    while (section > 0) {
+        const v = section % 10;
+        if (v === 0) {
+            if (!zero) {
+                zero = true;
+                chnStr = chnNumChar[v] + chnStr;
+            }
+        } else {
+            zero = false;
+            strIns = chnNumChar[v];
+            strIns += chnUnitChar[unitPos];
+            chnStr = strIns + chnStr;
+        }
+        unitPos++;
+        section = Math.floor(section / 10);
+    }
+    return chnStr;
+}
+const numberToChinese = function (num) {
+    let unitPos = 0;
+    let strIns = "", chnStr = "";
+    let needZero = false;
+
+    if (num === 0) {
+        return chnNumChar[0];
+    }
+
+    while (num > 0) {
+        const section = num % 10000;
+        if (needZero) {
+            chnStr = chnNumChar[0] + chnStr;
+        }
+        strIns = sectionToChinese(section);
+        strIns +=
+            section !== 0 ? chnUnitSection[unitPos] : chnUnitSection[0];
+        chnStr = strIns + chnStr;
+        needZero = section < 1000 && section > 0;
+        num = Math.floor(num / 10000);
+        unitPos++;
+    }
+
+    return chnStr;
+}
+const showQuestionParse = function (q) {
+    openParse.value = true
+    parseQuestion.value = q
+    parseCopy.value = {...q}
+}
+const setQuestionParse = function () {
+    openParse.value = false
+    const {parse, answer1, answer2} = parseCopy.value
+    parseQuestion.value.parse = parse
+    parseQuestion.value.answer1 = answer1
+    parseQuestion.value.answer2 = answer2
+}
+const showScore = function (data, index) {
+    const doc = document.getElementById("parent_score_" + index);
+    doc.style.display = "block";
+}
+const hideScore = function (data, index) {
+    const doc = document.getElementById("parent_score_" + index);
+    doc.style.display = "none";
+}
+
+const setGroupScore = function () {
+    const group = groupedQuestions.value[groupIdx.value];
+    const len = group.list.length;
+    group.score = len * groupScore.value;
+    for (let q of group.list) {
+        q.score = groupScore.value;
+    }
+    openGroupScore.value = false;
+}
+
+const setScore = function () {
+    const group = groupedQuestions.value[groupIdx.value]
+    const q = group.list[qIdx.value]
+    q.score = qScore.value
+    group.score = group.list.reduce((a, b) => a + b.score, 0)
+    openScore.value = false
+}
+
+const showQuestionScore = function (gIdx, qIdx) {
+    const doc = document.getElementById("score_" + gIdx + "_" + qIdx);
+    doc.style.display = "block";
+}
+const hideQuestionScore = function (gIdx, qIdx) {
+    const doc = document.getElementById("score_" + gIdx + "_" + qIdx);
+    doc.style.display = "none";
+}
+const deleteType = function (gIdx) {
+    groupedQuestions.value.splice(gIdx, 1);
+}
+const deleteQuestion = function (gIdx, qIdx) {
+    const group = groupedQuestions.value[gIdx]
+    group.list.splice(qIdx, 1)
+    if (group.list.length) {
+        group.num = group.list.length
+        group.score = group.list.reduce((a, b) => a + b.score, 0)
+    } else {
+        groupedQuestions.value.splice(gIdx, 1)
+    }
+}
+
+// 拖动组
+const draggingGroup = ref(null)
+const handleDragStart = function (e, group) {
+    draggingGroup.value = group
+}
+const handleDragEnd = function (e, group) {
+    draggingGroup.value = null;
+}
+const handleDragOver = function (e) {
+    e.dataTransfer.dropEffect = "move"; // e.dataTransfer.dropEffect="move";//在dragenter中针对放置目标来设置!
+}
+const handleDragEnter = function (e, group) {
+    e.dataTransfer.effectAllowed = "move"; //为需要移动的元素设置dragstart事件
+    if (group === draggingGroup.value) {
+        return;
+    }
+    const newItems = groupedQuestions.value;
+    const src = newItems.indexOf(draggingGroup.value);
+    const dst = newItems.indexOf(group);
+
+    newItems.splice(dst, 0, ...newItems.splice(src, 1))
+}
+// 拖动题
+const draggingQuestion = ref(null)
+const draggingQIdx = ref(0)
+const handleQuestionDragStart = function (e, q, i) {
+    draggingQuestion.value = q
+    draggingQIdx.value = i
+}
+const handleQuestionDragEnd = function (e, q) {
+    draggingQuestion.value = null
+    draggingQIdx.value = null
+}
+const handleQuestionDragOver = function (e) {
+    e.dataTransfer.dropEffect = "move"; // e.dataTransfer.dropEffect="move";//在dragenter中针对放置目标来设置!
+}
+const handleQuestionDragEnter = function (e, q, gIdx, qIdx) {
+    e.dataTransfer.effectAllowed = "move"; //为需要移动的元素设置dragstart事件
+    if (q === draggingQuestion.value && qIdx == draggingQIdx) {
+        return;
+    }
+    const newItems = groupedQuestions.value[gIdx].list;
+    const src = draggingQIdx.value;
+    const dst = qIdx;
+
+    newItems.splice(dst, 0, ...newItems.splice(src, 1));
+}
+
+const handleSavePaper = async function () {
+    const commit = toCommitPaper()
+    // validation
+    if (!commit.name) return ElMessage.error('请填写试卷名称')
+    if (!commit.questions?.length) return ElMessage.error('试卷至少包含1道试题')
+
+    // save
+    // TODO: 看看这个编辑有没有包含自动组卷的情况
+    await buildPaperManual(commit)
+    ElMessage.success('保存成功')
+}
 </script>
 
 <style scoped>

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

@@ -41,11 +41,22 @@ export const useProvidePaperExactCondition = function () {
         }
     }
 
+    const paperArgs = computed(() => ({
+        batchId: toValue(batchId),
+        batchName: batchList.value.find(b => b.batchId == batchId.value)?.name,
+        subjectId: toValue(subjectId),
+        subjectName: subjects.value.find(s => s.subjectId == subjectId.value)?.subjectName,
+        examTypes: [toValue(examType)],
+        location: toValue(location),
+        universityIds: [toValue(universityId)],
+        planIds: [toValue(majorPlanId)]
+    }))
     const payload = {
         location, provinces, examType, examTypes, universityId, universities,
         batchId, batchList, majorPlanId, majors, subjects, subjectId,
         knowledges, knowledgeNode, knowledgeId, knowledgeCheckNodes, knowledgeIds,
-        removeKnowledge, onKnowledgeRemove: knowledgeRemoveEvent.on
+        removeKnowledge, onKnowledgeRemove: knowledgeRemoveEvent.on,
+        paperArgs
     }
     provideLocal(key, payload)
 

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

@@ -25,10 +25,17 @@ export const useProvidePaperFullCondition = function () {
         }
     }
 
+    const paperArgs = computed(() => ({
+        batchId: toValue(batchId),
+        batchName: batchList.value.find(b => b.batchId == batchId.value)?.name,
+        subjectId: toValue(subjectId),
+        subjectName: subjects.value.find(s => s.subjectId == subjectId.value)?.subjectName
+    }))
     const payload = {
         batchId, batchList, subjectId, subjects,
         knowledges, knowledgeId, knowledgeNode, knowledgeCheckNodes, knowledgeIds,
-        removeKnowledge, onKnowledgeRemove: knowledgeRemoveEvent.on
+        removeKnowledge, onKnowledgeRemove: knowledgeRemoveEvent.on,
+        paperArgs
     }
     provideLocal(key, payload)
 

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

@@ -21,7 +21,7 @@ export const useProvidePaperQuestionCondition = function (exactMode, allowMultip
     const questionList = ref([])
 
     const {
-        batchId, subjectId, subjects, knowledgeId, knowledgeIds
+        subjectId, subjects, knowledgeId, knowledgeIds
     } = exactMode ? useInjectPaperExactCondition() : useInjectPaperFullCondition()
     const loading = useInjectLoading()
 

+ 41 - 0
back-ui/src/views/dz/papers/hooks/usePaperStorage.js

@@ -0,0 +1,41 @@
+import {useStorage} from "@vueuse/core";
+
+const key = 'PaperStorage'
+export const usePaperStorage = function (body) {
+    const bodyStr = JSON.stringify(body)
+    const paper = useStorage(key, bodyStr)
+    paper.value = bodyStr
+    return {paper}
+}
+
+export const usePaperResolver = function () {
+    const paper = useStorage(key, null)
+    const paperLocal = ref(JSON.parse(paper.value))
+    const groupedQuestions = ref([])
+    const resolvePaper = (function () {
+        const results = {}
+        paperLocal.value.questions.forEach(q => {
+            if (!results[q.qtpye]) results[q.qtpye] = []
+            results[q.qtpye].push(q)
+        })
+        groupedQuestions.value = Object.keys(results).map(g => ({
+            title: g,
+            list: results[g],
+            num: results[g].length,
+            score: results[g].reduce((acc, current) => acc + current.score, 0)
+        }))
+    })()
+
+    const toCommitPaper = function () {
+        const questions = []
+        groupedQuestions.value.forEach(g => questions.push(...g.list))
+        return {
+            ...paperLocal.value,
+            questions
+        }
+    }
+
+    return {
+        paperLocal, groupedQuestions, resolvePaper, toCommitPaper
+    }
+}