Browse Source

调整修改 exam 模块

reghao 1 week ago
parent
commit
bdbbc13d33

+ 13 - 5
src/api/exam.js

@@ -8,7 +8,7 @@ const examApi = {
   // 试卷接口
   // 试卷接口
   examPaper: '/api/content/exam/paper',
   examPaper: '/api/content/exam/paper',
   // 测评管理接口
   // 测评管理接口
-  examMgr: '/api/content/exam/mgr',
+  examUser: '/api/content/exam/eval_user',
   // 测评接口
   // 测评接口
   examEval: '/api/content/exam/eval',
   examEval: '/api/content/exam/eval',
   // 阅卷接口
   // 阅卷接口
@@ -70,9 +70,17 @@ export function getPaper(queryParams) {
 }
 }
 
 
 // *********************************************************************************************************************
 // *********************************************************************************************************************
-// 测评管理接口
-export function createExamEval(jsonData) {
-  return get(examApi.examMgr + '/create', jsonData)
+// 测评用户接口
+export function getCandidateUsers(queryParams) {
+  return get(examApi.examUser + '/candidate', queryParams)
+}
+
+export function addEvalUsers(jsonData) {
+  return post(examApi.examUser, jsonData)
+}
+
+export function getEvalUsers(queryParams) {
+  return get(examApi.examUser, queryParams)
 }
 }
 
 
 // *********************************************************************************************************************
 // *********************************************************************************************************************
@@ -108,7 +116,7 @@ export function getMarkPapers() {
 }
 }
 
 
 export function getExamMarkList(queryParams) {
 export function getExamMarkList(queryParams) {
-  return get(examApi.examMark + '/list', queryParams)
+  return get(examApi.examMark + '/paper_list', queryParams)
 }
 }
 
 
 export function getMarkView(queryParams) {
 export function getMarkView(queryParams) {

+ 1 - 1
src/router/exam.js

@@ -9,11 +9,11 @@ const ExamQuestionAdd = () => import('views/exam/ExamQuestionAdd')
 const ExamPaperList = () => import('views/exam/ExamPaperList')
 const ExamPaperList = () => import('views/exam/ExamPaperList')
 const ExamPaperAssembly = () => import('views/exam/ExamPaperAssembly')
 const ExamPaperAssembly = () => import('views/exam/ExamPaperAssembly')
 const ExamEvalList = () => import('views/exam/ExamEvalList')
 const ExamEvalList = () => import('views/exam/ExamEvalList')
+const ExamPaperResult = () => import('views/exam/ExamPaperResult')
 const ExamScoreIndex = () => import('views/exam/ExamScoreIndex')
 const ExamScoreIndex = () => import('views/exam/ExamScoreIndex')
 const ExamMarkIndex = () => import('views/exam/ExamMarkIndex')
 const ExamMarkIndex = () => import('views/exam/ExamMarkIndex')
 const ExamPaperPreview = () => import('views/exam/ExamPaperPreview')
 const ExamPaperPreview = () => import('views/exam/ExamPaperPreview')
 const ExamPaperMark = () => import('views/exam/ExamPaperMark')
 const ExamPaperMark = () => import('views/exam/ExamPaperMark')
-const ExamPaperResult = () => import('views/exam/ExamPaperResult')
 
 
 export default {
 export default {
   path: '/exam',
   path: '/exam',

+ 2 - 2
src/views/exam/Exam.vue

@@ -49,12 +49,12 @@
           <el-submenu index="/exam/user">
           <el-submenu index="/exam/user">
             <template slot="title">
             <template slot="title">
               <i class="el-icon-user" />
               <i class="el-icon-user" />
-              <span slot="title">我的考试</span>
+              <span slot="title">我的测评</span>
             </template>
             </template>
             <el-menu-item-group>
             <el-menu-item-group>
               <el-menu-item index="/exam/eval">
               <el-menu-item index="/exam/eval">
                 <i class="el-icon-film" />
                 <i class="el-icon-film" />
-                <span slot="title">考试列表</span>
+                <span slot="title">测评列表</span>
               </el-menu-item>
               </el-menu-item>
               <el-menu-item index="/exam/score">
               <el-menu-item index="/exam/score">
                 <i class="el-icon-film" />
                 <i class="el-icon-film" />

+ 12 - 12
src/views/exam/ExamDashboard1.vue

@@ -34,9 +34,9 @@
             placeholder="选择图表"
             placeholder="选择图表"
             @change="onSelectChange"
             @change="onSelectChange"
           >
           >
-            <el-option label="考试通过率" value="1" />
-            <el-option label="考试次数占比" value="2" />
-            <el-option label="考试通过率折线图" value="3" />
+            <el-option label="测评通过率" value="1" />
+            <el-option label="测评次数占比" value="2" />
+            <el-option label="测评通过率折线图" value="3" />
             <el-option label="新用户激活数" value="4" />
             <el-option label="新用户激活数" value="4" />
           </el-select>
           </el-select>
         </div>
         </div>
@@ -75,9 +75,9 @@ export default {
   name: 'ExamDashboard',
   name: 'ExamDashboard',
   data() {
   data() {
     return {
     return {
-      // 考试名称
+      // 测评名称
       examNames: [],
       examNames: [],
-      // 考试通过率
+      // 测评通过率
       passRate: [],
       passRate: [],
       // 饼图的数据
       // 饼图的数据
       pieData: [],
       pieData: [],
@@ -137,11 +137,11 @@ export default {
         }
         }
       })
       })
     },
     },
-    // 考试通过率柱状图
+    // 测评通过率柱状图
     drawLine() {
     drawLine() {
       this.chartOption = {
       this.chartOption = {
         title: {
         title: {
-          text: '考试通过率',
+          text: '测评通过率',
           subtext: 'dashbord1',
           subtext: 'dashbord1',
           x: 'center',
           x: 'center',
           y: 'top',
           y: 'top',
@@ -164,7 +164,7 @@ export default {
       this.chartOption = {
       this.chartOption = {
         // 标题
         // 标题
         title: {
         title: {
-          text: '考试通过率折线图',
+          text: '测评通过率折线图',
           x: 'center'
           x: 'center'
         },
         },
         // x轴
         // x轴
@@ -364,7 +364,7 @@ export default {
         }
         }
       })
       })
     },
     },
-    // 获取考试次数数据
+    // 获取测评次数数据
     async getExamNumbers() {
     async getExamNumbers() {
       await getExamCount().then((resp) => {
       await getExamCount().then((resp) => {
         const examNames = resp.data[0].split(',')
         const examNames = resp.data[0].split(',')
@@ -378,13 +378,13 @@ export default {
         this.drawPie()
         this.drawPie()
       })
       })
     },
     },
-    // 考试次数饼图
+    // 测评次数饼图
     drawPie() {
     drawPie() {
       // 基于准备好的dom,初始化echarts实例
       // 基于准备好的dom,初始化echarts实例
       const myChart = this.$echarts.init(document.getElementById('chart2'))
       const myChart = this.$echarts.init(document.getElementById('chart2'))
       const option = {
       const option = {
         title: {
         title: {
-          text: '考试次数占比',
+          text: '测评次数占比',
           subtext: 'dashbord2',
           subtext: 'dashbord2',
           x: 'center'
           x: 'center'
         },
         },
@@ -399,7 +399,7 @@ export default {
         },
         },
         series: [
         series: [
           {
           {
-            name: '考试次数',
+            name: '测评次数',
             type: 'pie',
             type: 'pie',
             radius: '55%',
             radius: '55%',
             data: this.pieData,
             data: this.pieData,

+ 19 - 24
src/views/exam/ExamEvalList.vue

@@ -7,8 +7,8 @@
           v-model="queryInfo.subjectId"
           v-model="queryInfo.subjectId"
           clearable
           clearable
           placeholder="请选择科目"
           placeholder="请选择科目"
-          @change="subjectChange"
           size="medium"
           size="medium"
+          @change="subjectChange"
         >
         >
           <el-option
           <el-option
             v-for="item in allSubject"
             v-for="item in allSubject"
@@ -89,14 +89,14 @@
                 size="mini"
                 size="mini"
                 type="primary"
                 type="primary"
                 icon="el-icon-edit"
                 icon="el-icon-edit"
-                @click="prepareExam(scope.$index, scope.row)"
+                @click="prepareExam(scope.row)"
               >去测评</el-button>
               >去测评</el-button>
               <el-button
               <el-button
                 v-else-if="scope.row.evalStatus === 2"
                 v-else-if="scope.row.evalStatus === 2"
                 size="mini"
                 size="mini"
                 type="danger"
                 type="danger"
                 icon="el-icon-s-claim"
                 icon="el-icon-s-claim"
-                @click="markPaper(scope.$index, scope.row)"
+                @click="viewPaper(scope.row)"
               >进行中</el-button>
               >进行中</el-button>
               <el-button
               <el-button
                 v-else-if="scope.row.evalStatus === 3"
                 v-else-if="scope.row.evalStatus === 3"
@@ -104,15 +104,14 @@
                 type="warning"
                 type="warning"
                 icon="el-icon-loading"
                 icon="el-icon-loading"
                 disabled
                 disabled
-                @click="waitMark(scope.$index, scope.row)"
+                @click="waitMark(scope.row)"
               >阅卷中</el-button>
               >阅卷中</el-button>
               <el-button
               <el-button
                 v-else-if="scope.row.evalStatus === 4"
                 v-else-if="scope.row.evalStatus === 4"
                 size="mini"
                 size="mini"
-                type="warning"
-                icon="el-icon-loading"
-                disabled
-                @click="waitMark(scope.$index, scope.row)"
+                type="success"
+                icon="el-icon-info"
+                @click="viewResult(scope.row)"
               >看结果</el-button>
               >看结果</el-button>
             </template>
             </template>
           </el-table-column>
           </el-table-column>
@@ -173,8 +172,8 @@
       </div>
       </div>
 
 
       <span slot="footer" class="dialog-footer">
       <span slot="footer" class="dialog-footer">
-        <el-button @click="startExamDialog = false" size="medium">返 回</el-button>
-        <el-button type="primary" @click="startExam(currentSelectedExam.examId)" size="medium">开始测评</el-button>
+        <el-button size="medium" @click="startExamDialog = false">返 回</el-button>
+        <el-button type="primary" size="medium" @click="startExam(currentSelectedExam.examId)">开始测评</el-button>
       </span>
       </span>
     </el-dialog>
     </el-dialog>
   </el-container>
   </el-container>
@@ -259,7 +258,7 @@ export default {
       this.queryInfo.pageSize = this.pageSize
       this.queryInfo.pageSize = this.pageSize
       this.getData(this.queryInfo)
       this.getData(this.queryInfo)
     },
     },
-    prepareExam(index, row) {
+    prepareExam(row) {
       if (!row.needPasscode) {
       if (!row.needPasscode) {
         this.startExamDialog = true
         this.startExamDialog = true
         this.currentSelectedExam = row
         this.currentSelectedExam = row
@@ -289,29 +288,25 @@ export default {
       })
       })
       window.open(routeUrl.href, '_blank')
       window.open(routeUrl.href, '_blank')
     },
     },
-    waitMark(index, row) {
+    waitMark(row) {
       this.$message.info('试卷待批改')
       this.$message.info('试卷待批改')
     },
     },
-    markPaper(index, row) {
-      const paperId = row.examId
+    viewPaper(row) {
+      const examId = row.examId
       const routeUrl = this.$router.resolve({
       const routeUrl = this.$router.resolve({
-        path: '/exam/paper/preview',
+        path: '/exams',
         query: {
         query: {
-          paperId: paperId,
-          viewType: 3,
-          userId: 0
+          examId: examId
         }
         }
       })
       })
       window.open(routeUrl.href, '_blank')
       window.open(routeUrl.href, '_blank')
     },
     },
-    viewResult(index, row) {
-      const paperId = row.examId
+    viewResult(row) {
+      const examId = row.examId
       const routeUrl = this.$router.resolve({
       const routeUrl = this.$router.resolve({
-        path: '/exam/paper/preview',
+        path: '/exam/eval/result',
         query: {
         query: {
-          paperId: paperId,
-          viewType: 4,
-          userId: ''
+          examId: examId
         }
         }
       })
       })
       window.open(routeUrl.href, '_blank')
       window.open(routeUrl.href, '_blank')

+ 1 - 1
src/views/exam/ExamMarkIndex.vue

@@ -44,7 +44,7 @@
         />
         />
         <el-table-column
         <el-table-column
           prop="totalScore"
           prop="totalScore"
-          label="考试总分"
+          label="测评总分"
         />
         />
         <el-table-column
         <el-table-column
           prop="evalUser"
           prop="evalUser"

+ 3 - 5
src/views/exam/ExamPaper.vue

@@ -38,7 +38,7 @@
       <div class="paper-header">
       <div class="paper-header">
         <h2 class="paper-title">{{ paperInfo.title }}</h2>
         <h2 class="paper-title">{{ paperInfo.title }}</h2>
         <div class="paper-meta">
         <div class="paper-meta">
-          <span>考试科目:{{ paperInfo.subjectName || '未指定' }}</span>
+          <span>测评科目:{{ paperInfo.subjectName || '未指定' }}</span>
         </div>
         </div>
       </div>
       </div>
 
 
@@ -290,8 +290,7 @@ export default {
               } else {
               } else {
                 localAnswers[q.questionId] = ''
                 localAnswers[q.questionId] = ''
               }
               }
-            }
-            else if (q.typeCode === 5 && q.children) {
+            } else if (q.typeCode === 5 && q.children) {
               q.children.forEach(subQ => {
               q.children.forEach(subQ => {
                 if (subQ.answers && !subQ.options) {
                 if (subQ.answers && !subQ.options) {
                   subQ.options = subQ.answers
                   subQ.options = subQ.answers
@@ -350,7 +349,6 @@ export default {
 
 
           this.answers = updatedAnswers
           this.answers = updatedAnswers
         }
         }
-
       } catch (error) {
       } catch (error) {
         this.$message.error(error.message || '加载试卷数据失败')
         this.$message.error(error.message || '加载试卷数据失败')
         console.error(error)
         console.error(error)
@@ -391,7 +389,7 @@ export default {
           this.remainTime--
           this.remainTime--
         } else {
         } else {
           clearInterval(this.timer)
           clearInterval(this.timer)
-          this.$alert('考试时间已到,系统已自动提交!', '提示', {
+          this.$alert('测评时间已到,系统已自动提交!', '提示', {
             confirmButtonText: '确定',
             confirmButtonText: '确定',
             callback: () => { this.submitExam() }
             callback: () => { this.submitExam() }
           })
           })

+ 113 - 18
src/views/exam/ExamPaperAssembly.vue

@@ -1,31 +1,65 @@
 <template>
 <template>
   <el-container class="paper-assembly-container">
   <el-container class="paper-assembly-container">
     <el-main>
     <el-main>
-      <el-page-header content="组卷控制台" @back="$emit('back')" />
+      <el-page-header content="组卷控制台" @back="handleBack" />
       <el-divider />
       <el-divider />
 
 
-      <el-form ref="paperForm" :model="paperForm" :rules="rules" label-width="100px" size="medium">
+      <el-form ref="paperForm" :model="paperForm" :rules="rules" label-width="100px" size="medium" class="optimized-paper-form">
+        <!-- 第一行:试卷主名称(通栏,突出核心信息) -->
         <el-row :gutter="20">
         <el-row :gutter="20">
-          <el-col :span="10">
+          <el-col :span="24">
             <el-form-item label="试卷名称" prop="title">
             <el-form-item label="试卷名称" prop="title">
-              <el-input v-model="paperForm.title" placeholder="如:2026年高考模拟考试理科综合能力测试" />
+              <el-input v-model="paperForm.title" placeholder="如:2026年高考模拟测评理科综合能力测试" clearable />
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
-          <el-col :span="6">
-            <el-form-item label="考试科目" prop="subjectId">
-              <el-select v-model="paperForm.subjectId" placeholder="请选择" style="width:100%">
-                <el-option
-                  v-for="item in allSubject"
-                  :key="item.label"
-                  :label="item.label"
-                  :value="item.value"
-                />
-              </el-select>
-            </el-form-item>
+        </el-row>
+
+        <!-- 第二行:核心元数据 + 总分看板(通过 flex 布局实现两端对齐) -->
+        <el-row :gutter="20" type="flex" justify="space-between" align="top">
+          <!-- 左侧:三个并排的参数输入框 -->
+          <el-col :span="18">
+            <el-row :gutter="20">
+              <el-col :span="8">
+                <el-form-item label="测评科目" prop="subjectId">
+                  <el-select v-model="paperForm.subjectId" placeholder="请选择科目" style="width:100%">
+                    <el-option
+                      v-for="item in allSubject"
+                      :key="item.label"
+                      :label="item.label"
+                      :value="item.value"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+
+              <!-- 修正了原代码中 duration 的 prop 误写为 title 的 Bug -->
+              <el-col :span="8">
+                <el-form-item label="测评时长" prop="duration">
+                  <el-input v-model="paperForm.duration" placeholder="输入分钟" type="number">
+                    <template slot="append">分钟</template>
+                  </el-input>
+                </el-form-item>
+              </el-col>
+
+              <!-- 修正了原代码中 startTime 的 prop 误写为 title 的 Bug -->
+              <el-col :span="8">
+                <el-form-item label="测评时间" prop="startTime">
+                  <el-date-picker
+                    v-model="paperForm.startTime"
+                    type="datetime"
+                    placeholder="开始时间"
+                    style="width: 100%"
+                  />
+                </el-form-item>
+              </el-col>
+            </el-row>
           </el-col>
           </el-col>
-          <el-col :span="8" class="score-summary-zone">
+
+          <!-- 右侧:独立的得分荣誉看板,与左侧等高对齐 -->
+          <el-col :span="6" class="score-summary-zone">
             <div class="total-score-badge">
             <div class="total-score-badge">
-              试卷总分:<span>{{ totalScore }}</span> 分
+              <div class="badge-label">试卷总分</div>
+              <div class="badge-value"><span>{{ totalScore }}</span>分</div>
             </div>
             </div>
           </el-col>
           </el-col>
         </el-row>
         </el-row>
@@ -194,11 +228,14 @@ export default {
       paperForm: {
       paperForm: {
         title: '',
         title: '',
         subjectId: '',
         subjectId: '',
+        duration: 0,
+        startTime: null,
+        totalScore: 0,
         sections: [] // 试卷大题结构
         sections: [] // 试卷大题结构
       },
       },
       rules: {
       rules: {
         title: [{ required: true, message: '请填写试卷名称', trigger: 'blur' }],
         title: [{ required: true, message: '请填写试卷名称', trigger: 'blur' }],
-        subjectId: [{ required: true, message: '请选择考试科目', trigger: 'change' }]
+        subjectId: [{ required: true, message: '请选择测评科目', trigger: 'change' }]
       },
       },
       // 题目选择器相关状态
       // 题目选择器相关状态
       selectorVisible: false,
       selectorVisible: false,
@@ -393,6 +430,7 @@ export default {
           return this.$message.error('存在没有添加题目的空大题,请检查或移除!')
           return this.$message.error('存在没有添加题目的空大题,请检查或移除!')
         }
         }
 
 
+        this.paperForm.totalScore = this.totalScore
         addPaper(this.paperForm).then(resp => {
         addPaper(this.paperForm).then(resp => {
           if (resp.code === 0) {
           if (resp.code === 0) {
             this.$message.success('试卷组装完毕,已成功保存至系统!')
             this.$message.success('试卷组装完毕,已成功保存至系统!')
@@ -405,6 +443,14 @@ export default {
           this.$message.error(error.message)
           this.$message.error(error.message)
         })
         })
       })
       })
+    },
+    handleBack() {
+      if (this.isChanged) {
+        this.$confirm('数据尚未保存,确定要返回吗?', '提示', { type: 'warning' })
+          .then(() => this.$router.push('/exam/paper')).catch(() => {})
+      } else {
+        this.$router.push('/exam/paper')
+      }
     }
     }
   }
   }
 }
 }
@@ -519,4 +565,53 @@ export default {
   border-top: 1px solid #ebeef5;
   border-top: 1px solid #ebeef5;
   text-align: center;
   text-align: center;
 }
 }
+
+.optimized-paper-form {
+  background: #fafbfe;
+  padding: 20px;
+  border-radius: 8px;
+  border: 1px solid #ebeef5;
+}
+
+/* 调整右侧总分区域的 Flex 布局,使其完美贴合右侧顶端 */
+.score-summary-zone {
+  display: flex;
+  justify-content: flex-end;
+  align-items: flex-start;
+  /* 移除原有的固定 height: 47px,让它自适应弹性盒 */
+}
+
+/* 重新设计更具现代感的总分 Badge */
+.total-score-badge {
+  background: linear-gradient(135deg, #ffeded 0%, #ffe3e3 100%);
+  border: 1px solid #ffcbcb;
+  padding: 8px 18px;
+  border-radius: 6px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-width: 120px;
+  box-shadow: 0 2px 6px rgba(245, 108, 108, 0.1);
+}
+
+.total-score-badge .badge-label {
+  font-size: 12px;
+  color: #f56c6c;
+  margin-bottom: 2px;
+  letter-spacing: 1px;
+}
+
+.total-score-badge .badge-value {
+  font-size: 13px;
+  color: #f56c6c;
+  font-weight: bold;
+}
+
+.total-score-badge .badge-value span {
+  font-size: 28px;
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  margin-right: 2px;
+  line-height: 1;
+}
 </style>
 </style>

+ 257 - 40
src/views/exam/ExamPaperList.vue

@@ -43,12 +43,12 @@
         />
         />
         <el-table-column
         <el-table-column
           prop="startTime"
           prop="startTime"
-          label="考试时间"
+          label="测评时间"
           width="150"
           width="150"
         />
         />
         <el-table-column
         <el-table-column
           prop="duration"
           prop="duration"
-          label="考试时长(分钟)"
+          label="测评时长(分钟)"
           width="150"
           width="150"
         />
         />
         <el-table-column
         <el-table-column
@@ -124,15 +124,25 @@
           width="320"
           width="320"
         >
         >
           <template slot-scope="scope">
           <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="info"
+              @click="evalUser(scope.row)"
+            >测评用户</el-button>
             <el-button
             <el-button
               size="mini"
               size="mini"
               type="warning"
               type="warning"
-              @click="previewPaper(scope.$index, scope.row)"
+              @click="previewPaper(scope.row)"
             >预览</el-button>
             >预览</el-button>
+            <el-button
+              size="mini"
+              type="info"
+              @click="printPaper(scope.row)"
+            >打印</el-button>
             <el-button
             <el-button
               size="mini"
               size="mini"
               type="warning"
               type="warning"
-              @click="handleDelete(scope.$index, scope.row)"
+              @click="handleDelete(scope.row)"
             >删除</el-button>
             >删除</el-button>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
@@ -149,11 +159,101 @@
         @next-click="handleCurrentChange"
         @next-click="handleCurrentChange"
       />
       />
     </el-main>
     </el-main>
+
+    <!-- 3. 选择测评用户弹窗 -->
+    <!-- ================= 弹窗 1:当前已指派的测评用户列表 ================= -->
+    <el-dialog
+      title="已指派测评用户管理"
+      :visible.sync="userDialogVisible"
+      width="75%"
+      @close="handleUserDialogClose"
+    >
+      <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
+        <div>
+          当前试卷:<span style="color: #409EFF; font-weight: bold;">{{ currentSelectPaper.paperName }}</span>
+        </div>
+        <!-- 触发第二级弹窗的按钮 -->
+        <el-button
+          type="primary"
+          icon="el-icon-plus"
+          size="small"
+          @click="openCandidateDialog"
+        >添加测评用户</el-button>
+      </div>
+
+      <!-- 已加入用户表格(展示当前参加测评的用户) -->
+      <el-table
+        v-loading="userLoading"
+        :data="allUserList"
+        border
+        size="small"
+        height="350px"
+        style="width: 100%"
+      >
+        <el-table-column type="index" label="序号" width="60" align="center" />
+        <el-table-column prop="userId" label="用户 ID" width="120" />
+        <el-table-column prop="username" label="用户名" />
+        <!-- 可以在这里预留删除/移出当前测评用户的动作按钮 -->
+      </el-table>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button size="small" type="primary" @click="userDialogVisible = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- ================= 弹窗 2:选择候选用户(添加新用户) ================= -->
+    <el-dialog
+      title="添加需要参加测评的用户"
+      :visible.sync="candidateDialogVisible"
+      width="65%"
+      append-to-body
+      @close="handleCandidateDialogClose"
+    >
+      <!-- 搜索过滤栏 -->
+      <div style="margin-bottom: 15px;">
+        <el-input
+          v-model="candidateQuery.keyword"
+          placeholder="输入用户名搜索候选用户..."
+          prefix-icon="el-icon-search"
+          size="small"
+          style="width: 260px; margin-right: 10px;"
+          clearable
+          @input="getCandidateList"
+        />
+        <el-button type="primary" size="small" @click="getCandidateList">查询</el-button>
+      </div>
+
+      <!-- 候选用户多选表格 -->
+      <el-table
+        ref="candidateTableRef"
+        v-loading="candidateLoading"
+        :data="candidateUserList"
+        border
+        size="small"
+        height="300px"
+        style="width: 100%"
+        @selection-change="handleCandidateSelectionChange"
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column prop="userId" label="用户 ID" width="120" />
+        <el-table-column prop="username" label="用户名" />
+      </el-table>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button size="small" @click="candidateDialogVisible = false">取 消</el-button>
+        <el-button
+          size="small"
+          type="success"
+          :disabled="selectedCandidates.length === 0"
+          @click="submitAddEvalUsers"
+        >确认添加 (已选 {{ selectedCandidates.length }} 人)</el-button>
+      </div>
+    </el-dialog>
   </el-container>
   </el-container>
 </template>
 </template>
 
 
 <script>
 <script>
-import { getSubjectKV, getPapers, deletePaper } from '@/api/exam'
+import { getSubjectKV, getPapers, deletePaper, addEvalUsers, getEvalUsers, getCandidateUsers } from '@/api/exam'
 
 
 export default {
 export default {
   name: 'ExamPaperList',
   name: 'ExamPaperList',
@@ -171,7 +271,25 @@ export default {
         pageNumber: 1,
         pageNumber: 1,
         pageSize: 10
         pageSize: 10
       },
       },
-      allSubject: []
+      allSubject: [],
+      // ****************** 新增:用户分配相关变量 ******************
+      userDialogVisible: false, // 控制弹窗显示
+      userLoading: false, // 表格加载状态
+      currentSelectPaper: {}, // 当前操作的行对象
+      allUserList: [], // 穿梭框/表格里的所有用户数据集
+      selectedUsers: [], // 当前勾选的用户数组
+      userQuery: { // 用户过滤条件
+        keyword: ''
+      },
+      // ****************** 级联弹窗相关状态变量 ******************
+      // 弹窗 2 (候选用户) 变量
+      candidateDialogVisible: false,
+      candidateLoading: false,
+      candidateUserList: [], // 接口返回的候选学生列表
+      selectedCandidates: [], // 在弹窗2中勾选中的待添加学生
+      candidateQuery: {
+        keyword: '' // 候选人关键字检索
+      }
     }
     }
   },
   },
   created() {
   created() {
@@ -194,20 +312,10 @@ export default {
           this.dataList = resp.data.list
           this.dataList = resp.data.list
           this.totalSize = resp.data.totalSize
           this.totalSize = resp.data.totalSize
         } else {
         } else {
-          this.$notify({
-            title: '提示',
-            message: resp.msg,
-            type: 'warning',
-            duration: 3000
-          })
+          this.$message.error(resp.msg)
         }
         }
       }).catch(error => {
       }).catch(error => {
-        this.$notify({
-          title: '提示',
-          message: error.message,
-          type: 'error',
-          duration: 3000
-        })
+        this.$message.error(error.message)
       })
       })
     },
     },
     getSubjects() {
     getSubjects() {
@@ -224,42 +332,151 @@ export default {
       this.queryInfo.pageSize = this.pageSize
       this.queryInfo.pageSize = this.pageSize
       this.getData(this.queryInfo)
       this.getData(this.queryInfo)
     },
     },
-    handleDelete(index, row) {
+    /* ==================== 弹窗 1:已指派用户管理逻辑 ==================== */
+    // 点击主表格“测评用户”按钮触发
+    evalUser(row) {
+      this.currentSelectPaper = row
+      this.userDialogVisible = true
+      this.getJoinedUserList()
+    },
+
+    // 动态拉取当前试卷已经存在的测评用户
+    getJoinedUserList() {
+      this.userLoading = true
+      const queryParams = {
+        paperId: this.currentSelectPaper.paperId
+      }
+      getEvalUsers(queryParams).then(resp => {
+        if (resp.code === 0) {
+          this.allUserList = resp.data || []
+        } else {
+          this.$message.warning(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error('拉取已指派用户失败: ' + error.message)
+      }).finally(() => {
+        this.userLoading = false
+      })
+    },
+
+    // 弹窗 1 关闭时销毁缓存
+    handleUserDialogClose() {
+      this.allUserList = []
+      this.currentSelectPaper = {}
+    },
+    /* ==================== 弹窗 2:候选用户选择与添加逻辑 ==================== */
+    // 打开添加候选人弹窗
+    openCandidateDialog() {
+      this.candidateDialogVisible = true
+      this.getCandidateList()
+    },
+
+    // 获取可供选择的候选用户列表
+    getCandidateList() {
+      this.candidateLoading = true
+      const queryParams = {
+        paperId: this.currentSelectPaper.paperId,
+        keyword: this.candidateQuery.keyword // 传递搜索关键词给后端(若后端支持)
+      }
+
+      getCandidateUsers(queryParams).then(resp => {
+        if (resp.code === 0) {
+          const respData = resp.data || []
+          // 如果后端接口不支持 keyword 筛选,则由前端进行客户端纯文本过滤:
+          if (this.candidateQuery.keyword) {
+            this.candidateUserList = respData.filter(u =>
+              u.username.includes(this.candidateQuery.keyword)
+            )
+          } else {
+            this.candidateUserList = respData
+          }
+        } else {
+          this.$message.warning(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error('拉取候选用户失败: ' + error.message)
+      }).finally(() => {
+        this.candidateLoading = false
+      })
+    },
+
+    // 监听候选人表格的勾选改变
+    handleCandidateSelectionChange(val) {
+      this.selectedCandidates = val
+    },
+
+    // 确认将勾选的候选人添加至测评
+    submitAddEvalUsers() {
+      const paperId = this.currentSelectPaper.paperId
+      const userIds = this.selectedCandidates.map(item => item.userId)
+
+      const jsonData = {
+        paperId: paperId,
+        userIds: userIds
+      }
+
+      addEvalUsers(jsonData).then(resp => {
+        if (resp.code === 0) {
+          this.$message.success(`成功为试卷添加了 ${userIds.length} 个测评用户!`)
+          this.candidateDialogVisible = false // 关闭候选弹窗
+          this.getJoinedUserList() // 刷新弹窗1中的已加入用户列表,形成无缝视觉联动
+        } else {
+          this.$message.warning(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error('添加失败: ' + error.message)
+      })
+    },
+
+    // 弹窗 2 关闭时重置状态
+    handleCandidateDialogClose() {
+      this.candidateQuery.keyword = ''
+      this.selectedCandidates = []
+      this.candidateUserList = []
+      if (this.$refs.candidateTableRef) {
+        this.$refs.candidateTableRef.clearSelection()
+      }
+    },
+    previewPaper(row) {
+      const paperId = row.paperId
+      const routeUrl = this.$router.resolve({
+        path: '/exam/paper/preview',
+        query: {
+          paperId: paperId
+        }
+      })
+      window.open(routeUrl.href, '_blank')
+    },
+    printPaper(row) {
+      this.$message.info('打印试卷未实现')
+      /* const paperId = row.paperId
+      const routeUrl = this.$router.resolve({
+        path: '/exams_print',
+        query: {
+          paperId: paperId
+        }
+      })
+      window.open(routeUrl.href, '_blank')
+      */
+    },
+    handleDelete(row) {
       this.$confirm('确定要删除 ' + row.paperName + '?', '提示', {
       this.$confirm('确定要删除 ' + row.paperName + '?', '提示', {
         confirmButtonText: '确定',
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         cancelButtonText: '取消',
         type: 'warning'
         type: 'warning'
       }).then(() => {
       }).then(() => {
         deletePaper(row.paperId).then(resp => {
         deletePaper(row.paperId).then(resp => {
+          this.$message.info(resp.msg)
           if (resp.code === 0) {
           if (resp.code === 0) {
-            this.$notify({
-              title: '提示',
-              message: '稿件已删除',
-              type: 'warning',
-              duration: 3000
-            })
             this.$router.go(0)
             this.$router.go(0)
           }
           }
         })
         })
-      }).catch(() => {
-        this.$message({
-          type: 'info',
-          message: '已取消'
-        })
+      }).catch((error) => {
+        this.$message.error(error.message)
       })
       })
     },
     },
     addExamPaper() {
     addExamPaper() {
       this.$router.push('/exam/paper/add')
       this.$router.push('/exam/paper/add')
-    },
-    previewPaper(index, row) {
-      const paperId = row.paperId
-      const routeUrl = this.$router.resolve({
-        path: '/exam/paper/preview',
-        query: {
-          paperId: paperId
-        }
-      })
-      window.open(routeUrl.href, '_blank')
     }
     }
   }
   }
 }
 }

+ 1 - 1
src/views/exam/ExamPaperMark.vue

@@ -31,7 +31,7 @@
       <div class="paper-header">
       <div class="paper-header">
         <h2 class="paper-title">{{ paperInfo.title }}【阅卷流水线】</h2>
         <h2 class="paper-title">{{ paperInfo.title }}【阅卷流水线】</h2>
         <div class="paper-meta">
         <div class="paper-meta">
-          <span>考试科目:{{ paperInfo.subjectName || '未指定' }}</span>
+          <span>测评科目:{{ paperInfo.subjectName || '未指定' }}</span>
         </div>
         </div>
       </div>
       </div>
 
 

+ 5 - 5
src/views/exam/ExamPaperResult.vue

@@ -32,7 +32,7 @@
       <div class="paper-header">
       <div class="paper-header">
         <h2 class="paper-title">{{ paperInfo.title }}</h2>
         <h2 class="paper-title">{{ paperInfo.title }}</h2>
         <div class="paper-meta">
         <div class="paper-meta">
-          <span>考试科目:{{ paperInfo.subjectName || '未指定' }}</span>
+          <span>测评科目:{{ paperInfo.subjectName || '未指定' }}</span>
           <span class="total-score-banner">
           <span class="total-score-banner">
             客观/主观题得分总览请见各题下方
             客观/主观题得分总览请见各题下方
           </span>
           </span>
@@ -297,7 +297,7 @@ export default {
   data() {
   data() {
     return {
     return {
       queryParams: {
       queryParams: {
-        resultId: 0
+        examId: 0
       },
       },
       currentActiveId: null,
       currentActiveId: null,
       paperInfo: {},
       paperInfo: {},
@@ -306,12 +306,12 @@ export default {
   },
   },
   created() {
   created() {
     document.title = '试卷结果'
     document.title = '试卷结果'
-    const resultId = this.$route.query.resultId
-    if (!resultId) {
+    const examId = this.$route.query.examId
+    if (!examId) {
       this.$message.error('参数缺失,无法加载试卷结果页面!')
       this.$message.error('参数缺失,无法加载试卷结果页面!')
       return
       return
     }
     }
-    this.queryParams.resultId = resultId
+    this.queryParams.examId = examId
     this.loadPaperData()
     this.loadPaperData()
   },
   },
   methods: {
   methods: {

+ 2 - 2
src/views/exam/ExamQuestionAdd.vue

@@ -412,9 +412,9 @@ export default {
     handleBack() {
     handleBack() {
       if (this.isChanged) {
       if (this.isChanged) {
         this.$confirm('数据尚未保存,确定要返回吗?', '提示', { type: 'warning' })
         this.$confirm('数据尚未保存,确定要返回吗?', '提示', { type: 'warning' })
-          .then(() => this.$emit('back')).catch(() => {})
+          .then(() => this.$router.push('/exam/question')).catch(() => {})
       } else {
       } else {
-        this.$emit('back')
+        this.$router.push('/exam/question')
       }
       }
     },
     },
     backToList() { this.handleBack() }
     backToList() { this.handleBack() }

+ 1 - 7
src/views/exam/ExamSubject.vue

@@ -1,10 +1,5 @@
 <template>
 <template>
   <el-container>
   <el-container>
-    <el-header height="220">
-      <el-row style="margin-top: 10px">
-        <el-button type="plain" icon="el-icon-plus" @click="addSubjectVisible = true">添加</el-button>
-      </el-row>
-    </el-header>
     <el-main>
     <el-main>
       <el-table
       <el-table
         :data="dataList"
         :data="dataList"
@@ -73,8 +68,7 @@ export default {
       currentPage: 1,
       currentPage: 1,
       pageSize: 20,
       pageSize: 20,
       totalSize: 0,
       totalSize: 0,
-      dataList: [],
-      addSubjectVisible: false
+      dataList: []
     }
     }
   },
   },
   created() {
   created() {