dedsudiyu 1 week ago
parent
commit
57508cbe68

+ 608 - 0
src/views/safetyEducationExaminationNew/examManagement/examinationArrange/addPage.vue

@@ -0,0 +1,608 @@
+<!-- 新建考试 -->
+<template>
+  <div class="page-container examinationArrange-addPage">
+    <div class="top-max-big-box">
+      <p class="top-1-p">{{newData.id ? '编辑考试' : '新建考试'}}</p>
+      <p class="top-2-p" @click="backPage()">返回</p>
+      <p class="top-3-p" v-if="newData.id" @click="resetButton()">重置</p>
+      <p class="top-4-p" @click="submitButton()">保存</p>
+    </div>
+    <div class="content-box">
+      <div class="layout-box">
+        <!-- 左侧导航 -->
+        <div class="side-nav">
+          <div
+            v-for="tab in tabs"
+            :key="tab.key"
+            class="nav-item"
+            :class="{ active: activeTab === tab.key }"
+            @click="activeTab = tab.key"
+          >{{ tab.label }}</div>
+        </div>
+
+        <!-- 右侧内容 -->
+        <div class="tab-content scrollbar-box">
+
+          <!-- ==================== 基本设置 ==================== -->
+          <div v-show="activeTab === 'basic'">
+            <el-form :model="newData" ref="basicForm" label-width="100px" :rules="rules">
+              <div class="section-block">
+                <div class="flex-row-box">
+                  <el-form-item label="考试类型" prop="examType">
+                    <el-select v-model="newData.examType" placeholder="分级分类" style="width:200px;">
+                      <el-option v-for="item in optionExamType" :key="item.value" :label="item.label" :value="item.value" />
+                    </el-select>
+                  </el-form-item>
+                  <el-form-item label="风险等级" prop="riskLevel">
+                    <el-select v-model="newData.riskLevel" placeholder="I级" style="width:200px;">
+                      <el-option v-for="item in optionRiskLevel" :key="item.value" :label="item.label" :value="item.value" />
+                    </el-select>
+                  </el-form-item>
+                </div>
+
+                <el-form-item label="考试名称" prop="examName">
+                  <el-input v-model="newData.examName" placeholder="请输入课程名称" style="width:100%;" />
+                </el-form-item>
+
+                <el-form-item label="答卷次数" prop="answerCount">
+                  <div class="stepper-row">
+                    <div class="number-stepper">
+                      <button class="stepper-btn" @click.prevent="stepChange('answerCount', -1)">-</button>
+                      <span class="stepper-val">{{ newData.answerCount }}</span>
+                      <button class="stepper-btn" @click.prevent="stepChange('answerCount', 1)">+</button>
+                    </div>
+                    <span class="unit-text">次</span>
+                    <el-radio v-model="newData.answerUnlimited" :label="true" style="margin-left:20px;">无限制</el-radio>
+                  </div>
+                </el-form-item>
+
+                <el-form-item label="是否共享">
+                  <el-switch v-model="newData.isShare" :active-value="true" :inactive-value="false"
+                    active-color="#0183FB" inactive-color="#AAAAAA" />
+                  <span class="tip-text" v-if="newData.isShare">共享开启,全校所有学院单位都可见</span>
+                </el-form-item>
+
+                <el-form-item label="启用" prop="enabled">
+                  <el-switch v-model="newData.enabled" :active-value="true" :inactive-value="false"
+                    active-color="#0183FB" inactive-color="#AAAAAA" />
+                </el-form-item>
+
+                <el-form-item label="考卷" prop="paperId">
+                  <div class="paper-select-row">
+                    <el-button type="primary" @click="openPaperDrawer">选择试卷</el-button>
+                    <div class="selected-tag" v-if="newData.paperName">
+                      <span>{{ newData.paperName }}</span>
+                      <i class="el-icon-close" @click="newData.paperId = null; newData.paperName = ''"></i>
+                    </div>
+                  </div>
+                </el-form-item>
+              </div>
+
+              <div class="section-divider"></div>
+
+              <div class="section-block">
+                <div class="flex-row-box">
+                  <el-form-item label="考试时间" prop="examTimeRange">
+                    <el-date-picker
+                      v-model="newData.examTimeRange"
+                      type="datetimerange"
+                      range-separator="至"
+                      start-placeholder="开始日期"
+                      end-placeholder="结束日期"
+                      style="width:320px;"
+                    />
+                  </el-form-item>
+                  <el-form-item label="考试时长" prop="examDuration">
+                    <div class="stepper-row">
+                      <div class="number-stepper">
+                        <button class="stepper-btn" @click.prevent="stepChange('examDuration', -1)">-</button>
+                        <span class="stepper-val">{{ newData.examDuration }}</span>
+                        <button class="stepper-btn" @click.prevent="stepChange('examDuration', 1)">+</button>
+                      </div>
+                      <span class="unit-text">分钟</span>
+                      <span class="tip-text" style="margin-left:12px;">限制答卷最大时长</span>
+                    </div>
+                  </el-form-item>
+                </div>
+
+                <el-form-item label="考试范围" prop="examScope">
+                  <div class="scope-btn-group">
+                    <button
+                      v-for="item in scopeOptions"
+                      :key="item.value"
+                      class="scope-btn"
+                      :class="{ active: newData.examScope === item.value }"
+                      @click.prevent="newData.examScope = item.value"
+                    >
+                      <i v-if="newData.examScope === item.value" class="el-icon-check" style="color:#0183fa;margin-right:4px;"></i>
+                      {{ item.label }}
+                    </button>
+                  </div>
+                  <!-- 考试范围表格(非全校时显示) -->
+                  <div v-if="newData.examScope !== 'all'" class="scope-table-box">
+                    <div class="scope-table-header">已选取:{{ scopeList.length }}组</div>
+                    <el-table :data="scopeList" size="small" style="width:100%;">
+                      <el-table-column label="学院" prop="college" />
+                      <el-table-column label="专业" prop="major" width="160" />
+                      <el-table-column label="角色" prop="role" width="120" />
+                      <el-table-column label="年级" prop="grade" width="120" />
+                      <el-table-column label="操作" width="80" align="center">
+                        <template slot-scope="{ $index }">
+                          <el-button size="mini" @click="scopeList.splice($index, 1)">移除</el-button>
+                        </template>
+                      </el-table-column>
+                    </el-table>
+                  </div>
+                </el-form-item>
+              </div>
+            </el-form>
+          </div>
+
+          <!-- ==================== 考前配置 ==================== -->
+          <div v-show="activeTab === 'preExam'">
+            <el-form :model="newData" label-width="100px">
+              <div class="section-block">
+                <el-form-item label="学时要求">
+                  <div class="switch-row">
+                    <el-switch v-model="newData.creditRequired" :active-value="true" :inactive-value="false"
+                      active-color="#0183FB" inactive-color="#AAAAAA" />
+                    <div class="number-stepper" style="margin-left:12px;">
+                      <button class="stepper-btn" @click.prevent="stepChange('creditHours', -1)">-</button>
+                      <span class="stepper-val">{{ newData.creditHours }}</span>
+                      <button class="stepper-btn" @click.prevent="stepChange('creditHours', 1)">+</button>
+                    </div>
+                    <span class="unit-text">学时</span>
+                    <span class="tip-text" style="margin-left:12px;" v-if="newData.creditRequired">开启后,需完成相应学时才能参加考试</span>
+                  </div>
+                </el-form-item>
+
+                <el-form-item label="考前学习">
+                  <div class="switch-row">
+                    <el-switch v-model="newData.preStudy" :active-value="true" :inactive-value="false"
+                      active-color="#0183FB" inactive-color="#AAAAAA" />
+                    <el-button type="primary" style="margin-left:12px;" @click="openCourseDrawer">选择课程</el-button>
+                  </div>
+                  <div class="selected-tag" v-if="newData.preStudyCourseName" style="margin-top:8px;max-width:400px;">
+                    <span>{{ newData.preStudyCourseName }}</span>
+                    <i class="el-icon-close" @click="newData.preStudyCourseId = null; newData.preStudyCourseName = ''"></i>
+                  </div>
+                  <div class="tip-text" v-if="newData.preStudy" style="margin-top:6px;">开启后,需完成学习才能参加考试</div>
+                </el-form-item>
+
+                <el-form-item label="模拟练习">
+                  <div class="switch-row">
+                    <el-switch v-model="newData.mockPractice" :active-value="true" :inactive-value="false"
+                      active-color="#0183FB" inactive-color="#AAAAAA" />
+                    <el-button type="primary" style="margin-left:12px;" @click="reuseExamPaper">复用试卷</el-button>
+                    <el-button style="margin-left:8px;" @click="openMockPaperDrawer">选择试题</el-button>
+                    <span class="tip-text" style="margin-left:12px;" v-if="newData.mockPractice">开启后,需完成模拟练习任务才能参加考试</span>
+                  </div>
+                  <div class="mock-stat-bar" v-if="newData.mockPaperId">
+                    <span>总题目数:<b>{{ newData.mockTotalCount || 0 }}</b></span>
+                    <span style="margin-left:20px;">总分:<b>{{ newData.mockTotalScore || 0 }}</b></span>
+                    <span style="margin-left:20px;">及格分:<b>{{ newData.mockPassScore || 0 }}</b></span>
+                  </div>
+                </el-form-item>
+
+                <el-form-item label="考试承诺书">
+                  <div class="switch-row">
+                    <el-switch v-model="newData.pledge" :active-value="true" :inactive-value="false"
+                      active-color="#0183FB" inactive-color="#AAAAAA" />
+                    <span class="tip-text" style="margin-left:12px;" v-if="newData.pledge">开启后,考生需要阅读考试承诺内容并做出承诺才可开始考试</span>
+                    <span style="margin-left:30px;font-weight:600;color:#333;">承诺书打印</span>
+                    <el-switch v-model="newData.pledgePrint" :active-value="true" :inactive-value="false"
+                      active-color="#0183FB" inactive-color="#AAAAAA" style="margin-left:12px;" />
+                  </div>
+                  <div class="editor-box" v-if="newData.pledge" style="margin-top:12px;">
+                    <el-input
+                      type="textarea"
+                      v-model="newData.pledgeContent"
+                      :rows="10"
+                      placeholder="请输入考试承诺书内容"
+                    />
+                  </div>
+                </el-form-item>
+              </div>
+            </el-form>
+          </div>
+
+          <!-- ==================== 防作弊 ==================== -->
+          <div v-show="activeTab === 'antiCheat'">
+            <el-form :model="newData" label-width="120px">
+              <div class="section-block">
+                <el-form-item label="图形弹窗校验">
+                  <div class="switch-row">
+                    <el-switch v-model="newData.captcha" :active-value="true" :inactive-value="false"
+                      active-color="#0183FB" inactive-color="#AAAAAA" />
+                    <template v-if="newData.captcha">
+                      <span class="tip-text" style="margin-left:12px;">开启后,每隔</span>
+                      <div class="number-stepper" style="margin-left:8px;">
+                        <button class="stepper-btn" @click.prevent="stepChange('captchaInterval', -1)">-</button>
+                        <span class="stepper-val">{{ newData.captchaInterval }}</span>
+                        <button class="stepper-btn" @click.prevent="stepChange('captchaInterval', 1)">+</button>
+                      </div>
+                      <span class="tip-text" style="margin-left:8px;">分钟,提醒弹窗验证</span>
+                    </template>
+                    <span class="tip-text" style="margin-left:12px;" v-else>开启后,每隔 N 分钟,提醒弹窗验证</span>
+                  </div>
+                </el-form-item>
+
+                <el-form-item label="人脸识别校验">
+                  <div class="anti-cheat-group">
+                    <div class="switch-row" style="margin-bottom:12px;">
+                      <el-switch v-model="newData.faceRecognitionBefore" :active-value="true" :inactive-value="false"
+                        active-color="#0183FB" inactive-color="#AAAAAA" />
+                      <span class="tip-text" style="margin-left:12px;">开启后,考试前需要进行人脸识别(仅学习考试一体机)</span>
+                    </div>
+                    <div class="switch-row">
+                      <el-switch v-model="newData.faceRecognitionAfter" :active-value="true" :inactive-value="false"
+                        active-color="#0183FB" inactive-color="#AAAAAA" />
+                      <span class="tip-text" style="margin-left:12px;">开启后,考试后需要进行人脸识别(仅学习考试一体机)</span>
+                    </div>
+                  </div>
+                </el-form-item>
+
+                <el-form-item label="摄像头抓拍">
+                  <div class="switch-row">
+                    <el-switch v-model="newData.cameraCapture" :active-value="true" :inactive-value="false"
+                      active-color="#0183FB" inactive-color="#AAAAAA" />
+                    <span class="tip-text" style="margin-left:12px;">开启后,考试过程中摄像头随机抓拍(仅学习考试一体机)</span>
+                  </div>
+                </el-form-item>
+              </div>
+            </el-form>
+          </div>
+
+          <!-- ==================== 获得证书 ==================== -->
+          <div v-show="activeTab === 'certificate'">
+            <el-form :model="newData" label-width="120px">
+              <div class="section-block">
+                <el-form-item label="获得证书">
+                  <div class="switch-row">
+                    <el-switch v-model="newData.certificate" :active-value="true" :inactive-value="false"
+                      active-color="#0183FB" inactive-color="#AAAAAA" />
+                    <el-button type="primary" style="margin-left:12px;" @click="openCertTemplateDrawer">选择模板</el-button>
+                  </div>
+                </el-form-item>
+
+                <el-form-item label="证书打印">
+                  <el-switch v-model="newData.certPrint" :active-value="true" :inactive-value="false"
+                    active-color="#0183FB" inactive-color="#AAAAAA" />
+                </el-form-item>
+
+                <el-form-item label="证书有效期">
+                  <div class="stepper-row">
+                    <div class="number-stepper">
+                      <button class="stepper-btn" @click.prevent="stepChange('certValidity', -1)">-</button>
+                      <span class="stepper-val">{{ newData.certValidity }}</span>
+                      <button class="stepper-btn" @click.prevent="stepChange('certValidity', 1)">+</button>
+                    </div>
+                    <span class="unit-text">个月</span>
+                  </div>
+                </el-form-item>
+
+                <el-form-item label="证书复审周期">
+                  <div class="stepper-row">
+                    <div class="number-stepper">
+                      <button class="stepper-btn" @click.prevent="stepChange('certReviewCycle', -1)">-</button>
+                      <span class="stepper-val">{{ newData.certReviewCycle }}</span>
+                      <button class="stepper-btn" @click.prevent="stepChange('certReviewCycle', 1)">+</button>
+                    </div>
+                    <span class="unit-text">个月</span>
+                  </div>
+                </el-form-item>
+              </div>
+            </el-form>
+          </div>
+
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'addPage',
+    props: {
+      propsData: {}
+    },
+    data() {
+      return {
+        activeTab: 'basic',
+        tabs: [
+          { key: 'basic', label: '基本设置' },
+          { key: 'preExam', label: '考前配置' },
+          { key: 'antiCheat', label: '防作弊' },
+          { key: 'certificate', label: '获得证书' },
+        ],
+        scopeOptions: [
+          { label: '全校', value: 'all' },
+          { label: '考试范围', value: 'scope' },
+          { label: '指定考生', value: 'specify' },
+        ],
+        optionExamType: [
+          { label: '分级分类', value: 1 },
+        ],
+        optionRiskLevel: [
+          { label: 'I级', value: 1 },
+          { label: 'II级', value: 2 },
+          { label: 'III级', value: 3 },
+        ],
+        scopeList: [],
+        newData: {
+          id: null,
+          examType: null,
+          riskLevel: null,
+          examName: '',
+          answerCount: 2,
+          answerUnlimited: false,
+          isShare: true,
+          enabled: true,
+          paperId: null,
+          paperName: '',
+          examTimeRange: null,
+          examDuration: 2,
+          examScope: 'scope',
+          creditRequired: false,
+          creditHours: 2,
+          preStudy: false,
+          preStudyCourseId: null,
+          preStudyCourseName: '',
+          mockPractice: false,
+          mockPaperId: null,
+          mockTotalCount: 0,
+          mockTotalScore: 0,
+          mockPassScore: 0,
+          pledge: false,
+          pledgePrint: false,
+          pledgeContent: '',
+          captcha: false,
+          captchaInterval: 2,
+          faceRecognitionBefore: false,
+          faceRecognitionAfter: false,
+          cameraCapture: false,
+          certificate: true,
+          certPrint: true,
+          certValidity: 12,
+          certReviewCycle: 12,
+        },
+        rules: {
+          examType: [{ required: true, message: '请选择考试类型', trigger: 'change' }],
+          examName: [{ required: true, message: '请输入考试名称', trigger: 'blur' }],
+          paperId: [{ required: true, message: '请选择考卷', trigger: 'change' }],
+          examTimeRange: [{ required: true, message: '请选择考试时间', trigger: 'change' }],
+        },
+      }
+    },
+    mounted() {
+      this.initialize()
+    },
+    methods: {
+      initialize() {
+        if (this.propsData && this.propsData.id) {
+          this.newData = { ...this.newData, ...this.propsData }
+        }
+      },
+      backPage() {
+        this.$parent.tableButton(6)
+      },
+      resetButton() {
+        this.$refs.basicForm && this.$refs.basicForm.resetFields()
+      },
+      submitButton() {
+        this.$refs.basicForm.validate(valid => {
+          if (!valid) {
+            this.activeTab = 'basic'
+            return
+          }
+          // TODO: 调用保存接口
+        })
+      },
+      stepChange(field, delta) {
+        const val = (Number(this.newData[field]) || 0) + delta
+        this.newData[field] = Math.max(0, val)
+      },
+      openPaperDrawer() {
+        // TODO: 打开选择试卷弹窗
+      },
+      openCourseDrawer() {
+        // TODO: 打开选择课程弹窗
+      },
+      reuseExamPaper() {
+        // TODO: 复用考试试卷
+      },
+      openMockPaperDrawer() {
+        // TODO: 打开选择试题弹窗
+      },
+      openCertTemplateDrawer() {
+        // TODO: 打开选择证书模板弹窗
+      },
+    }
+  }
+</script>
+
+<style scoped lang="scss">
+  .examinationArrange-addPage {
+    .top-max-big-box {
+      height: 60px;
+      display: flex;
+      overflow: hidden;
+      padding: 0 20px;
+      border-bottom: 1px solid #dedede;
+      align-items: center;
+      .top-1-p { flex: 1; font-size: 16px; font-weight: 700; }
+      .top-2-p {
+        height: 35px; line-height: 33px; margin-left: 20px; padding: 0 20px;
+        border-radius: 4px; background-color: #fff; border: 1px solid #999; color: #999; cursor: pointer;
+      }
+      .top-3-p {
+        height: 35px; line-height: 33px; margin-left: 20px; padding: 0 20px;
+        border-radius: 4px; background-color: #dedede; border: 1px solid #dedede; color: #333; cursor: pointer;
+      }
+      .top-4-p {
+        height: 35px; line-height: 33px; margin-left: 20px; padding: 0 20px;
+        border-radius: 4px; background-color: #0183fa; border: 1px solid #0183fa; color: #fff; cursor: pointer;
+      }
+    }
+
+    .content-box {
+      flex: 1;
+      display: flex;
+      overflow: hidden;
+    }
+
+    .layout-box {
+      display: flex;
+      width: 100%;
+      height: 100%;
+    }
+
+    .side-nav {
+      width: 140px;
+      flex-shrink: 0;
+      border-right: 1px solid #e8e8e8;
+      padding: 24px 0;
+      background: #fff;
+      .nav-item {
+        padding: 14px 24px;
+        font-size: 14px;
+        color: #555;
+        cursor: pointer;
+        transition: all 0.15s;
+        &:hover { color: #0183fa; }
+        &.active {
+          color: #0183fa;
+          font-weight: 600;
+          background: #f0f7ff;
+          border-right: 3px solid #0183fa;
+        }
+      }
+    }
+
+    .tab-content {
+      flex: 1;
+      overflow-y: auto;
+      padding: 24px 32px;
+    }
+
+    .section-block {
+      padding-bottom: 8px;
+    }
+
+    .section-divider {
+      height: 1px;
+      background: #e8e8e8;
+      margin: 16px 0 24px;
+    }
+
+    .flex-row-box {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 0 60px;
+    }
+
+    .stepper-row {
+      display: flex;
+      align-items: center;
+    }
+
+    .number-stepper {
+      display: inline-flex;
+      align-items: center;
+      border: 1px solid #dcdfe6;
+      border-radius: 4px;
+      overflow: hidden;
+      .stepper-btn {
+        width: 32px; height: 32px; background: #f5f7fa; border: none;
+        cursor: pointer; font-size: 16px; color: #333;
+        &:hover { background: #e6f0fe; }
+      }
+      .stepper-val {
+        min-width: 44px; text-align: center; font-size: 14px; padding: 0 4px;
+      }
+    }
+
+    .unit-text {
+      margin-left: 8px; color: #333; white-space: nowrap; font-size: 14px;
+    }
+
+    .tip-text {
+      color: #999; font-size: 13px;
+    }
+
+    .paper-select-row {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+      flex-wrap: wrap;
+    }
+
+    .selected-tag {
+      display: inline-flex;
+      align-items: center;
+      background: #e6f4ff;
+      border: 1px solid #91caff;
+      border-radius: 4px;
+      padding: 6px 12px;
+      color: #0183fa;
+      font-size: 13px;
+      min-width: 200px;
+      max-width: 500px;
+      i { margin-left: 8px; cursor: pointer; &:hover { color: #e74c3c; } }
+    }
+
+    .scope-btn-group {
+      display: flex;
+      gap: 8px;
+      margin-bottom: 12px;
+      .scope-btn {
+        padding: 7px 20px; border: 1px solid #dcdfe6; border-radius: 4px;
+        background: #fff; cursor: pointer; font-size: 14px; color: #333;
+        display: flex; align-items: center; transition: all 0.15s;
+        &:hover { border-color: #0183fa; color: #0183fa; }
+        &.active { border-color: #0183fa; color: #0183fa; background: #f0f7ff; }
+      }
+    }
+
+    .scope-table-box {
+      border: 1px solid #e8e8e8;
+      border-radius: 4px;
+      overflow: hidden;
+      .scope-table-header {
+        background: #f5f5f5;
+        padding: 10px 16px;
+        font-size: 13px;
+        color: #555;
+        border-bottom: 1px solid #e8e8e8;
+      }
+    }
+
+    .switch-row {
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+    }
+
+    .anti-cheat-group {
+      display: flex;
+      flex-direction: column;
+    }
+
+    .mock-stat-bar {
+      background: #e6f4ff;
+      border: 1px solid #91caff;
+      border-radius: 4px;
+      padding: 10px 16px;
+      margin-top: 10px;
+      font-size: 13px;
+      color: #333;
+      display: inline-block;
+      b { color: #0183fa; font-weight: 600; }
+    }
+
+    .editor-box {
+      max-width: 760px;
+    }
+  }
+</style>

+ 5 - 10
src/views/safetyEducationExaminationNew/examManagement/examinationArrange/index.vue

@@ -79,7 +79,6 @@
           <p class="page-submit-common-style-button"
           <p class="page-submit-common-style-button"
              style="float: right;"
              style="float: right;"
              @click="tableButton(1)"
              @click="tableButton(1)"
-             v-hasPermiRouter="['demo:demo:add']"
           >新建考试</p>
           >新建考试</p>
         </el-form>
         </el-form>
       </div>
       </div>
@@ -103,19 +102,15 @@
                 <p class="table-button-null"></p>
                 <p class="table-button-null"></p>
                 <p class="table-button-p"
                 <p class="table-button-p"
                    @click="tableButton(2,scope.row)"
                    @click="tableButton(2,scope.row)"
-                   v-hasPermiRouter="['demo:demo:detail']"
                 >补考</p>
                 >补考</p>
                 <p class="table-button-p"
                 <p class="table-button-p"
                    @click="tableButton(3,scope.row)"
                    @click="tableButton(3,scope.row)"
-                   v-hasPermiRouter="['demo:demo:edit']"
                 >编辑</p>
                 >编辑</p>
                 <p class="table-button-p"
                 <p class="table-button-p"
                    @click="tableButton(2,scope.row)"
                    @click="tableButton(2,scope.row)"
-                   v-hasPermiRouter="['demo:demo:detail']"
                 >复制</p>
                 >复制</p>
                 <p class="table-button-p"
                 <p class="table-button-p"
                    @click="tableButton(4,scope.row)"
                    @click="tableButton(4,scope.row)"
-                   v-hasPermiRouter="['demo:demo:del']"
                 >删除</p>
                 >删除</p>
                 <p class="table-button-null"></p>
                 <p class="table-button-null"></p>
               </div>
               </div>
@@ -131,20 +126,20 @@
         />
         />
       </div>
       </div>
     </div>
     </div>
-    <!--<add-page :propsData="propsData" v-if="pageType === 2"></add-page>-->
+    <add-page :propsData="propsData" v-if="pageType === 2"></add-page>
   </div>
   </div>
 </template>
 </template>
 <script>
 <script>
   //import { getDicts } from "@/api/commonality/noPermission";
   //import { getDicts } from "@/api/commonality/noPermission";
   //import { systemUserSelect } from "@/api/commonality/permission";
   //import { systemUserSelect } from "@/api/commonality/permission";
   //import { getInfo } from "@/api/basicsModules/index";
   //import { getInfo } from "@/api/basicsModules/index";
-  //import addPage from "./addPage.vue";
+  import addPage from "./addPage.vue";
   import { examElDisciplineTypeList,examElExamTypeList,systemDeptCurrentDept, } from "@/api/safetyEducationExaminationNew/index";
   import { examElDisciplineTypeList,examElExamTypeList,systemDeptCurrentDept, } from "@/api/safetyEducationExaminationNew/index";
   export default {
   export default {
     name: 'index',
     name: 'index',
-    //components: {
-    //  addPage
-    //},
+    components: {
+     addPage
+    },
     data () {
     data () {
       return {
       return {
         tableButtonType:this.hasPermiDom(['demo:demo:detail','demo:demo:edit','demo:demo:del',]),
         tableButtonType:this.hasPermiDom(['demo:demo:detail','demo:demo:edit','demo:demo:del',]),

+ 516 - 0
src/views/safetyEducationExaminationNew/examManagement/testPaperManagement/addPage.vue

@@ -0,0 +1,516 @@
+<!-- 新建试卷 -->
+<template>
+  <div class="page-container testPaperManagement-addPage">
+    <div class="top-max-big-box">
+      <p class="top-1-p">{{newData.id?'编辑试卷':'新建试卷'}}</p>
+      <p class="top-2-p" @click="backPage()">返回</p>
+      <p class="top-3-p" @click="resetButton()">重置</p>
+      <p class="top-4-p" @click="submitButton()">保存</p>
+    </div>
+    <div class="content-box scrollbar-box">
+      <el-form class="add-form-box" :model="newData" ref="form" label-width="100px" :rules="rules">
+
+        <!-- 第一块:基本信息 -->
+        <div class="section-box">
+          <!-- 试卷名称 -->
+          <el-form-item label="试卷名称" prop="name" class="full-width-item">
+            <el-input v-model="newData.name" placeholder="请输入名称" />
+          </el-form-item>
+
+          <!-- 总分数 / 及格分数 / 答题时间 -->
+          <div class="flex-row-box">
+            <el-form-item label="总分数" prop="totalScore">
+              <div class="input-unit-box">
+                <el-input v-model="newData.totalScore" placeholder="100" style="width:160px;" />
+                <span class="unit-text">分</span>
+              </div>
+            </el-form-item>
+            <el-form-item label="及格分数" prop="passScore">
+              <div class="input-unit-box">
+                <el-input v-model="newData.passScore" placeholder="90" style="width:160px;" />
+                <span class="unit-text">分</span>
+              </div>
+            </el-form-item>
+            <el-form-item label="答题时间" prop="duration">
+              <div class="input-unit-box">
+                <el-input v-model="newData.duration" placeholder="30" style="width:160px;" />
+                <span class="unit-text">分钟</span>
+              </div>
+            </el-form-item>
+          </div>
+
+          <!-- 专业类别 / 是否共享 -->
+          <div class="flex-row-box">
+            <el-form-item label="专业类别" prop="disciplineId">
+              <el-select v-model="newData.disciplineId" placeholder="专业类别" style="width:200px;">
+                <el-option
+                  v-for="item in optionListDiscipline"
+                  :key="item.id"
+                  :label="item.disciplineName"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="是否共享" prop="isShare">
+              <el-switch
+                v-model="newData.isShare"
+                :active-value="true"
+                :inactive-value="false"
+                active-color="#0183FB"
+                inactive-color="#AAAAAA"
+              />
+              <span class="share-tip">{{newData.isShare ? '共享开启,全校所有学院单位都可见' : ''}}</span>
+            </el-form-item>
+          </div>
+        </div>
+
+        <!-- 分隔线 -->
+        <div class="section-divider"></div>
+
+        <!-- 第二块:组卷方式 & 知识点 -->
+        <div class="section-box">
+          <!-- 组卷方式 -->
+          <el-form-item label="组卷方式" prop="groupType">
+            <el-radio-group v-model="newData.groupType">
+              <el-radio :label="1">固定组卷</el-radio>
+              <el-radio :label="2">随机组卷</el-radio>
+            </el-radio-group>
+          </el-form-item>
+
+          <!-- 选择知识点 -->
+          <el-form-item label="选择知识点" prop="knowledgePoints" class="knowledge-form-item">
+            <!-- 统计信息条 -->
+            <div class="stat-bar">
+              <span>总题目数:<b>{{totalQuestionCount}}</b></span>
+              <span style="margin-left:20px;">总分:<b>{{totalScore}}</b></span>
+              <span style="margin-left:20px;" v-if="remainScore > 0">还需设置 <b>{{remainScore}}</b> 分的题目</span>
+            </div>
+
+            <!-- 已选知识点标签 -->
+            <div class="knowledge-tag-box">
+              <div
+                v-for="(item, index) in selectedKnowledgePoints"
+                :key="item.id"
+                class="knowledge-tag"
+              >
+                <span>{{item.name}}</span>
+                <i class="el-icon-close" @click="removeKnowledge(index)"></i>
+              </div>
+              <el-cascader
+                ref="knowledgeCascader"
+                v-model="cascaderValue"
+                :options="optionListKnowledge"
+                :props="{
+                  emitPath: false,
+                  checkStrictly: true,
+                  value: 'id',
+                  label: 'knowledgePointName',
+                  children: 'children',
+                }"
+                :show-all-levels="false"
+                filterable
+                placeholder="选择知识点"
+                style="width:160px;"
+                @change="onKnowledgeSelect"
+              />
+            </div>
+
+            <!-- 知识点明细表格 -->
+            <div class="knowledge-table" v-if="selectedKnowledgePoints.length > 0">
+              <div class="table-header">
+                <span class="col-name">知识点</span>
+                <span class="col-count">试题数量</span>
+                <span class="col-score">每题分数</span>
+                <span class="col-fixed">固定选题</span>
+              </div>
+              <div
+                class="table-row"
+                v-for="(item, index) in selectedKnowledgePoints"
+                :key="item.id"
+              >
+                <span class="col-name link-text">{{item.name}}</span>
+                <span class="col-count link-text">{{item.questionCount || 0}}</span>
+                <span class="col-score">
+                  <div class="number-stepper">
+                    <button class="stepper-btn" @click="changeScore(index, -1)">-</button>
+                    <span class="stepper-val">{{item.scorePerQuestion || 0}}</span>
+                    <button class="stepper-btn" @click="changeScore(index, 1)">+</button>
+                  </div>
+                </span>
+                <span class="col-fixed">
+                  <el-input
+                    v-model="item.fixedCount"
+                    style="width:80px;"
+                    type="number"
+                    :min="0"
+                  />
+                  <el-button type="primary" circle icon="el-icon-plus" size="mini" style="margin-left:8px;" @click="addFixed(index)" />
+                  <el-button circle icon="el-icon-aim" size="mini" style="margin-left:4px;" @click="selectFixed(index)" />
+                </span>
+              </div>
+            </div>
+          </el-form-item>
+        </div>
+
+      </el-form>
+    </div>
+    <el-drawer
+      size="100%"
+      custom-class="testPaper-select-drawer"
+      :append-to-body="true"
+      :title="drawerKnowledgeName ? '固定选题 - ' + drawerKnowledgeName : '固定选题'"
+      ref="drawer"
+      :visible.sync="drawer"
+      direction="rtl"
+      :withHeader="false">
+      <drawer-item
+        v-if="drawer"
+        :knowledge-name="drawerKnowledgeName"
+        :knowledge-id="drawerKnowledgeId"
+        :score-per-question="drawerScorePerQuestion"
+        :total-score="Number(newData.totalScore) || 0"
+        :init-selected="drawerInitSelected"
+        @confirm="onDrawerConfirm"
+        @cancel="drawer = false"
+      />
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+  import drawerItem from './drawerItem.vue'
+  export default {
+    name: 'addPage',
+    components: { drawerItem },
+    props: {
+      propsData: {}
+    },
+    data() {
+      return {
+        drawer: false,
+        drawerKnowledgeName: '',
+        drawerKnowledgeId: null,
+        drawerScorePerQuestion: 1,
+        drawerInitSelected: [],
+        drawerCurrentIndex: -1,
+        newData: {
+          id: null,
+          name: '',
+          totalScore: '',
+          passScore: '',
+          duration: '',
+          disciplineId: null,
+          isShare: true,
+          groupType: 1,
+        },
+        rules: {
+          name: [{ required: true, message: '请输入试卷名称', trigger: 'blur' }],
+          totalScore: [{ required: true, message: '请输入总分数', trigger: 'blur' }],
+          passScore: [{ required: true, message: '请输入及格分数', trigger: 'blur' }],
+          duration: [{ required: true, message: '请输入答题时间', trigger: 'blur' }],
+          groupType: [{ required: true, message: '请选择组卷方式', trigger: 'change' }],
+          knowledgePoints: [{ required: true, message: '请选择知识点', trigger: 'change' }],
+        },
+        optionListDiscipline: [],
+        optionListKnowledge: [],
+        selectedKnowledgePoints: [],
+        cascaderValue: null,
+      }
+    },
+    computed: {
+      totalQuestionCount() {
+        return this.selectedKnowledgePoints.reduce((sum, item) => sum + (Number(item.fixedCount) || 0), 0)
+      },
+      totalScore() {
+        return this.selectedKnowledgePoints.reduce((sum, item) => {
+          return sum + (Number(item.fixedCount) || 0) * (Number(item.scorePerQuestion) || 0)
+        }, 0)
+      },
+      remainScore() {
+        return Math.max(0, Number(this.newData.totalScore || 0) - this.totalScore)
+      },
+    },
+    mounted() {
+      this.initialize()
+    },
+    methods: {
+      initialize() {
+        if (this.propsData && this.propsData.id) {
+          this.newData = { ...this.propsData }
+        }
+      },
+      backPage() {
+        this.$parent.tableButton(6)
+      },
+      resetButton() {
+        this.$set(this,'drawer',true);
+      },
+      submitButton() {
+        this.$refs.form.validate(valid => {
+          if (!valid) return
+          // TODO: 调用保存接口
+        })
+      },
+      onKnowledgeSelect(val) {
+        if (!val) return
+        // 防止重复添加
+        const exists = this.selectedKnowledgePoints.find(k => k.id === val)
+        if (!exists) {
+          // 从级联选项中找到对应节点名称
+          const node = this.$refs.knowledgeCascader.getCheckedNodes()
+          const name = node && node[0] ? node[0].label : String(val)
+          this.selectedKnowledgePoints.push({
+            id: val,
+            name,
+            questionCount: 0,
+            scorePerQuestion: 1,
+            fixedCount: 0,
+          })
+        }
+        this.$nextTick(() => {
+          this.cascaderValue = null
+        })
+      },
+      removeKnowledge(index) {
+        this.selectedKnowledgePoints.splice(index, 1)
+      },
+      changeScore(index, delta) {
+        const val = (Number(this.selectedKnowledgePoints[index].scorePerQuestion) || 0) + delta
+        this.selectedKnowledgePoints[index].scorePerQuestion = Math.max(0, val)
+      },
+      addFixed(index) {
+        // TODO: 打开固定选题弹窗
+      },
+      selectFixed(index) {
+        const item = this.selectedKnowledgePoints[index]
+        this.drawerCurrentIndex = index
+        this.drawerKnowledgeName = item.name
+        this.drawerKnowledgeId = item.id
+        this.drawerScorePerQuestion = Number(item.scorePerQuestion) || 1
+        this.drawerInitSelected = item.fixedList ? [...item.fixedList] : []
+        this.drawer = true
+      },
+      onDrawerConfirm(list) {
+        if (this.drawerCurrentIndex >= 0) {
+          const item = this.selectedKnowledgePoints[this.drawerCurrentIndex]
+          item.fixedList = list
+          item.fixedCount = list.length
+          this.$set(this.selectedKnowledgePoints, this.drawerCurrentIndex, { ...item })
+        }
+        this.drawer = false
+      },
+    }
+  }
+</script>
+
+<style scoped lang="scss">
+  .testPaperManagement-addPage {
+    .top-max-big-box {
+      height: 60px;
+      display: flex;
+      overflow: hidden;
+      padding: 0 20px;
+      border-bottom: 1px solid #dedede;
+      align-items: center;
+      .top-1-p {
+        flex: 1;
+        font-size: 16px;
+        font-weight: 700;
+      }
+      .top-2-p {
+        height: 35px;
+        line-height: 33px;
+        margin-left: 20px;
+        padding: 0 20px;
+        border-radius: 4px;
+        background-color: #fff;
+        border: 1px solid #999;
+        color: #999;
+        cursor: pointer;
+      }
+      .top-3-p {
+        height: 35px;
+        line-height: 33px;
+        margin-left: 20px;
+        padding: 0 20px;
+        border-radius: 4px;
+        background-color: #dedede;
+        border: 1px solid #dedede;
+        color: #333;
+        cursor: pointer;
+      }
+      .top-4-p {
+        height: 35px;
+        line-height: 33px;
+        margin-left: 20px;
+        padding: 0 20px;
+        border-radius: 4px;
+        background-color: #0183fa;
+        border: 1px solid #0183fa;
+        color: #fff;
+        cursor: pointer;
+      }
+    }
+
+    .content-box {
+      flex: 1;
+      overflow-y: auto;
+      padding: 20px;
+    }
+
+    .add-form-box {
+      .section-box {
+        padding: 20px 0;
+      }
+
+      .section-divider {
+        height: 1px;
+        background: #e8e8e8;
+        margin: 0 0 10px;
+      }
+
+      .full-width-item {
+        ::v-deep .el-input {
+          width: 100%;
+        }
+        ::v-deep .el-form-item__content {
+          width: calc(100% - 100px);
+        }
+      }
+
+      .flex-row-box {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 0 40px;
+      }
+
+      .input-unit-box {
+        display: flex;
+        align-items: center;
+        .unit-text {
+          margin-left: 8px;
+          color: #333;
+          white-space: nowrap;
+        }
+      }
+
+      .share-tip {
+        margin-left: 12px;
+        color: #999;
+        font-size: 13px;
+      }
+
+      /* 知识点区域 */
+      .knowledge-form-item {
+        ::v-deep .el-form-item__content {
+          width: calc(100% - 100px);
+        }
+      }
+
+      .stat-bar {
+        background: #f5f5f5;
+        padding: 10px 16px;
+        margin-bottom: 10px;
+        font-size: 14px;
+        color: #333;
+        b {
+          color: #0183fa;
+        }
+      }
+
+      .knowledge-tag-box {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+        align-items: center;
+        padding: 10px;
+        border: 1px solid #e8e8e8;
+        border-radius: 4px;
+        min-height: 48px;
+        margin-bottom: 16px;
+      }
+
+      .knowledge-tag {
+        display: inline-flex;
+        align-items: center;
+        background: #e6f4ff;
+        border: 1px solid #91caff;
+        color: #0183fa;
+        border-radius: 4px;
+        padding: 4px 10px;
+        font-size: 13px;
+        i {
+          margin-left: 6px;
+          cursor: pointer;
+          color: #0183fa;
+          font-size: 12px;
+          &:hover {
+            color: #e74c3c;
+          }
+        }
+      }
+
+      /* 知识点明细表格 */
+      .knowledge-table {
+        border: 1px solid #e8e8e8;
+        border-radius: 4px;
+        overflow: hidden;
+
+        .table-header,
+        .table-row {
+          display: flex;
+          align-items: center;
+          padding: 12px 16px;
+          .col-name  { flex: 3; }
+          .col-count { flex: 2; text-align: center; }
+          .col-score { flex: 2; text-align: center; }
+          .col-fixed { flex: 3; display: flex; align-items: center; }
+        }
+
+        .table-header {
+          background: #fafafa;
+          font-weight: 600;
+          font-size: 14px;
+          color: #333;
+          border-bottom: 1px solid #e8e8e8;
+        }
+
+        .table-row {
+          border-bottom: 1px solid #f0f0f0;
+          &:last-child { border-bottom: none; }
+        }
+
+        .link-text {
+          color: #0183fa;
+          cursor: pointer;
+        }
+
+        .number-stepper {
+          display: inline-flex;
+          align-items: center;
+          border: 1px solid #dcdfe6;
+          border-radius: 4px;
+          overflow: hidden;
+
+          .stepper-btn {
+            width: 32px;
+            height: 32px;
+            background: #f5f7fa;
+            border: none;
+            cursor: pointer;
+            font-size: 16px;
+            color: #333;
+            &:hover { background: #e6f0fe; }
+          }
+
+          .stepper-val {
+            min-width: 40px;
+            text-align: center;
+            font-size: 14px;
+            padding: 0 4px;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 361 - 0
src/views/safetyEducationExaminationNew/examManagement/testPaperManagement/drawerItem.vue

@@ -0,0 +1,361 @@
+<!-- 固定选题 Drawer 内容 -->
+<template>
+  <div class="drawerItem-container">
+    <!-- 顶部知识点标题 -->
+    <div class="drawer-header">
+      <span class="knowledge-title">{{ knowledgeName || '知识点名称' }}</span>
+    </div>
+
+    <div class="drawer-body">
+      <!-- 左侧:题库列表 -->
+      <div class="panel left-panel">
+        <div class="panel-inner">
+          <div class="filter-bar">
+            <el-select v-model="leftQuery.source" placeholder="来源" size="small" style="width:90px;" clearable>
+              <el-option v-for="item in optionSource" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+            <el-select v-model="leftQuery.type" placeholder="题型" size="small" style="width:90px;" clearable>
+              <el-option v-for="item in optionType" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+            <el-select v-model="leftQuery.difficulty" placeholder="难度" size="small" style="width:90px;" clearable>
+              <el-option v-for="item in optionDifficulty" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+            <el-input v-model="leftQuery.keyword" placeholder="搜索试题" size="small" style="width:140px;" clearable />
+            <el-button type="primary" size="small" @click="searchLeft">查询</el-button>
+            <el-button size="small" @click="resetLeft">重置</el-button>
+          </div>
+          <el-table
+            ref="leftTable"
+            :data="leftTableData"
+            size="small"
+            @selection-change="onLeftSelectionChange"
+            style="width:100%;"
+            height="calc(100vh - 220px)"
+          >
+            <el-table-column type="selection" width="40" />
+            <el-table-column label="题干" prop="stem" show-overflow-tooltip>
+              <template slot-scope="{ row }">
+                <span class="stem-text" v-html="row.stem || ''"></span>
+              </template>
+            </el-table-column>
+            <el-table-column label="题型" prop="typeName" width="80" align="center">
+              <template slot-scope="{ row }">
+                <el-tag size="mini" :type="typeTagMap[row.type]" effect="plain">{{ row.typeName }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="难度" prop="difficultyName" width="70" align="center">
+              <template slot-scope="{ row }">
+                <span :class="['difficulty-text', 'diff-' + row.difficulty]">{{ row.difficultyName }}</span>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </div>
+
+      <!-- 中间:穿梭按钮 -->
+      <div class="middle-actions">
+        <el-button circle class="transfer-btn" @click="moveToRight">
+          <i class="el-icon-arrow-right"></i>
+        </el-button>
+        <el-button circle class="transfer-btn" style="margin-left:0;" @click="moveToLeft">
+          <i class="el-icon-arrow-left"></i>
+        </el-button>
+      </div>
+
+      <!-- 右侧:已选题目 -->
+      <div class="panel right-panel">
+        <div class="panel-inner">
+          <div class="stat-bar">
+            <span>总题目数:<b>{{ rightTableData.length }}</b></span>
+            <span style="margin-left:20px;">总分:<b>{{ rightTotalScore }}</b></span>
+            <span v-if="remainScore > 0" style="margin-left:20px;color:#e6a23c;">
+              还需设置 <b>{{ remainScore }}</b> 分的题目
+            </span>
+          </div>
+          <div class="filter-bar">
+            <el-select v-model="rightQuery.source" placeholder="来源" size="small" style="width:90px;" clearable>
+              <el-option v-for="item in optionSource" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+            <el-select v-model="rightQuery.type" placeholder="题型" size="small" style="width:90px;" clearable>
+              <el-option v-for="item in optionType" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+            <el-select v-model="rightQuery.difficulty" placeholder="难度" size="small" style="width:90px;" clearable>
+              <el-option v-for="item in optionDifficulty" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+            <el-input v-model="rightQuery.keyword" placeholder="搜索试题" size="small" style="width:140px;" clearable />
+            <el-button type="primary" size="small" @click="searchRight">查询</el-button>
+            <el-button size="small" @click="resetRight">重置</el-button>
+          </div>
+          <el-table
+            ref="rightTable"
+            :data="rightTableDataFiltered"
+            size="small"
+            @selection-change="onRightSelectionChange"
+            style="width:100%;"
+            height="calc(100vh - 260px)"
+          >
+            <el-table-column type="selection" width="40" />
+            <el-table-column label="题干" prop="stem" show-overflow-tooltip>
+              <template slot-scope="{ row }">
+                <span class="stem-text" v-html="row.stem || ''"></span>
+              </template>
+            </el-table-column>
+            <el-table-column label="题型" prop="typeName" width="80" align="center">
+              <template slot-scope="{ row }">
+                <el-tag size="mini" :type="typeTagMap[row.type]" effect="plain">{{ row.typeName }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="难度" prop="difficultyName" width="70" align="center">
+              <template slot-scope="{ row }">
+                <span :class="['difficulty-text', 'diff-' + row.difficulty]">{{ row.difficultyName }}</span>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </div>
+    </div>
+
+    <!-- 底部操作栏 -->
+    <div class="drawer-footer">
+      <el-button style="line-height:40px;" @click="$emit('cancel')">取消</el-button>
+      <el-button type="primary"  style="line-height:40px;width:100px;" @click="confirmSelect">确认选题</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'drawerItem',
+    props: {
+      knowledgeName: { type: String, default: '' },
+      knowledgeId: { type: [String, Number], default: null },
+      scorePerQuestion: { type: Number, default: 1 },
+      totalScore: { type: Number, default: 0 },
+      initSelected: { type: Array, default: () => [] },
+    },
+    data() {
+      return {
+        leftQuery: { source: null, type: null, difficulty: null, keyword: '' },
+        rightQuery: { source: null, type: null, difficulty: null, keyword: '' },
+        leftTableData: [],
+        rightTableData: [],
+        leftSelected: [],
+        rightSelected: [],
+        optionSource: [
+          { label: '系统', value: 1 },
+          { label: '导入', value: 2 },
+        ],
+        optionType: [
+          { label: '单选题', value: 1 },
+          { label: '多选题', value: 2 },
+          { label: '判断题', value: 3 },
+        ],
+        optionDifficulty: [
+          { label: '简单', value: 1 },
+          { label: '一般', value: 2 },
+          { label: '较难', value: 3 },
+          { label: '极难', value: 4 },
+        ],
+        typeTagMap: { 1: '', 2: 'warning', 3: 'success' },
+      }
+    },
+    computed: {
+      rightTotalScore() {
+        return this.rightTableData.length * (this.scorePerQuestion || 1)
+      },
+      remainScore() {
+        return Math.max(0, (this.totalScore || 0) - this.rightTotalScore)
+      },
+      rightTableDataFiltered() {
+        return this.rightTableData.filter(row => {
+          const q = this.rightQuery
+          if (q.source && row.source !== q.source) return false
+          if (q.type && row.type !== q.type) return false
+          if (q.difficulty && row.difficulty !== q.difficulty) return false
+          if (q.keyword && !(row.stem || '').includes(q.keyword)) return false
+          return true
+        })
+      },
+    },
+    watch: {
+      initSelected: {
+        immediate: true,
+        handler(val) {
+          if (val && val.length) {
+            this.rightTableData = [...val]
+          }
+        },
+      },
+    },
+    mounted() {
+      this.fetchLeftData()
+    },
+    methods: {
+      fetchLeftData() {
+        // TODO: 调用接口获取题库列表
+      },
+      searchLeft() {
+        this.fetchLeftData()
+      },
+      resetLeft() {
+        this.leftQuery = { source: null, type: null, difficulty: null, keyword: '' }
+        this.fetchLeftData()
+      },
+      searchRight() {
+        this.$forceUpdate()
+      },
+      resetRight() {
+        this.rightQuery = { source: null, type: null, difficulty: null, keyword: '' }
+      },
+      onLeftSelectionChange(selection) {
+        this.leftSelected = selection
+      },
+      onRightSelectionChange(selection) {
+        this.rightSelected = selection
+      },
+      moveToRight() {
+        const rightIds = new Set(this.rightTableData.map(r => r.id))
+        const toAdd = this.leftSelected.filter(r => !rightIds.has(r.id))
+        this.rightTableData = [...this.rightTableData, ...toAdd]
+        this.$refs.leftTable.clearSelection()
+      },
+      moveToLeft() {
+        const removeIds = new Set(this.rightSelected.map(r => r.id))
+        this.rightTableData = this.rightTableData.filter(r => !removeIds.has(r.id))
+        this.$refs.rightTable.clearSelection()
+      },
+      confirmSelect() {
+        this.$emit('confirm', [...this.rightTableData])
+      },
+    },
+  }
+</script>
+
+<style scoped lang="scss">
+  .drawerItem-container {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    background: #f4f6fa;
+
+    .drawer-header {
+      padding: 16px 24px;
+      background: #fff;
+      border-bottom: 1px solid #e8e8e8;
+      flex-shrink: 0;
+      .knowledge-title {
+        font-size: 15px;
+        font-weight: 600;
+        color: #333;
+        &::before {
+          content: '';
+          display: inline-block;
+          width: 4px;
+          height: 16px;
+          background: #0183fa;
+          border-radius: 2px;
+          margin-right: 8px;
+          vertical-align: middle;
+        }
+      }
+    }
+
+    .drawer-body {
+      flex: 1;
+      display: flex;
+      overflow: hidden;
+      padding: 16px;
+      gap: 0;
+    }
+
+    .panel {
+      flex: 1;
+      background: #fff;
+      border-radius: 8px;
+      box-shadow: 0 1px 6px rgba(0, 0, 0, 0.06);
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+      .panel-inner {
+        display: flex;
+        flex-direction: column;
+        height: 100%;
+        padding: 16px;
+        overflow: hidden;
+      }
+    }
+
+    .filter-bar {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+      align-items: center;
+      margin-bottom: 12px;
+      flex-shrink: 0;
+    }
+
+    .stat-bar {
+      background: #f0f7ff;
+      border: 1px solid #d6eaff;
+      border-radius: 6px;
+      padding: 10px 16px;
+      margin-bottom: 12px;
+      font-size: 13px;
+      color: #555;
+      flex-shrink: 0;
+      b {
+        color: #0183fa;
+        font-weight: 600;
+      }
+    }
+
+    .middle-actions {
+      width: 60px;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      gap: 16px;
+      flex-shrink: 0;
+      padding: 0 8px;
+      .transfer-btn {
+        width: 44px;
+        height: 44px;
+        border: 2px solid #0183fa;
+        color: #0183fa;
+        background: #fff;
+        font-size: 16px;
+        padding: 0;
+        transition: all 0.2s;
+        &:hover {
+          background: #0183fa;
+          color: #fff;
+        }
+      }
+    }
+
+    .difficulty-text {
+      font-size: 12px;
+      font-weight: 500;
+      &.diff-1 { color: #67c23a; }
+      &.diff-2 { color: #e6a23c; }
+      &.diff-3 { color: #f56c6c; }
+      &.diff-4 { color: #c0392b; font-weight: 700; }
+    }
+
+    .stem-text {
+      color: #333;
+      font-size: 13px;
+    }
+
+    .drawer-footer {
+      padding: 14px 24px;
+      background: #fff;
+      border-top: 1px solid #e8e8e8;
+      display: flex;
+      justify-content: flex-end;
+      gap: 12px;
+      flex-shrink: 0;
+    }
+  }
+</style>

+ 5 - 5
src/views/safetyEducationExaminationNew/examManagement/testPaperManagement/index.vue

@@ -145,20 +145,20 @@
         />
         />
       </div>
       </div>
     </div>
     </div>
-    <!--<add-page :propsData="propsData" v-if="pageType === 2"></add-page>-->
+    <add-page :propsData="propsData" v-if="pageType === 2"></add-page>
   </div>
   </div>
 </template>
 </template>
 <script>
 <script>
   //import { getDicts } from "@/api/commonality/noPermission";
   //import { getDicts } from "@/api/commonality/noPermission";
   //import { systemUserSelect } from "@/api/commonality/permission";
   //import { systemUserSelect } from "@/api/commonality/permission";
   //import { getInfo } from "@/api/basicsModules/index";
   //import { getInfo } from "@/api/basicsModules/index";
-  //import addPage from "./addPage.vue";
+  import addPage from "./addPage.vue";
   import { examElDisciplineTypeList,systemDeptCurrentDept,examElKnowledgePointTreeList, } from "@/api/safetyEducationExaminationNew/index";
   import { examElDisciplineTypeList,systemDeptCurrentDept,examElKnowledgePointTreeList, } from "@/api/safetyEducationExaminationNew/index";
   export default {
   export default {
     name: 'index',
     name: 'index',
-    //components: {
-    //  addPage
-    //},
+    components: {
+     addPage
+    },
     data () {
     data () {
       return {
       return {
         tableButtonType:this.hasPermiDom(['demo:demo:detail','demo:demo:edit','demo:demo:del',]),
         tableButtonType:this.hasPermiDom(['demo:demo:detail','demo:demo:edit','demo:demo:del',]),

+ 317 - 25
src/views/safetyEducationExaminationNew/questionBankManagement/questionBankManagement/addPage.vue

@@ -7,17 +7,17 @@
       <p class="top-3-p" v-if="newData.id" @click="resetButton()">重置</p>
       <p class="top-3-p" v-if="newData.id" @click="resetButton()">重置</p>
       <p class="top-4-p" @click="submitButton()">保存</p>
       <p class="top-4-p" @click="submitButton()">保存</p>
     </div>
     </div>
-    <div class="content-box">
+    <div class="content-box scrollbar-box">
       <el-form class="add-form-box" :model="newData" ref="form" label-width="140px" :rules="rules">
       <el-form class="add-form-box" :model="newData" ref="form" label-width="140px" :rules="rules">
         <div class="top-content-box">
         <div class="top-content-box">
           <div class="flex-content-box">
           <div class="flex-content-box">
             <el-form-item label="试题类型" prop="data1">
             <el-form-item label="试题类型" prop="data1">
-              <el-select v-model="newData.data1" placeholder="试题类型" style="width: 180px">
+              <el-select v-model="newData.data1" @change="answersScreening()" placeholder="试题类型" style="width: 180px">
                 <el-option
                 <el-option
                   v-for="dict in optionListA"
                   v-for="dict in optionListA"
-                  :key="dict.id"
-                  :label="dict.disciplineName"
-                  :value="dict.id"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
                 />
                 />
               </el-select>
               </el-select>
             </el-form-item>
             </el-form-item>
@@ -42,9 +42,9 @@
               <el-select v-model="newData.data4" placeholder="难度" style="width: 180px">
               <el-select v-model="newData.data4" placeholder="难度" style="width: 180px">
                 <el-option
                 <el-option
                   v-for="dict in optionListC"
                   v-for="dict in optionListC"
-                  :key="dict.id"
-                  :label="dict.disciplineName"
-                  :value="dict.id"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
                 />
                 />
               </el-select>
               </el-select>
             </el-form-item>
             </el-form-item>
@@ -76,21 +76,72 @@
             />
             />
           </el-form-item>
           </el-form-item>
         </div>
         </div>
-        <div class="title-content-box">
+        <div class="title-content-box" v-if="newData.data1 == 1 || newData.data1 == 2">
           <p></p>
           <p></p>
           <p>选项</p>
           <p>选项</p>
           <p></p>
           <p></p>
         </div>
         </div>
-        <div class="title-content-box">
+        <div class="max-big-for-box" v-if="newData.data1 == 1 || newData.data1 == 2">
+          <p class="position-p" @click="addItem()">添加选项</p>
+          <div class="for-editor-form-box"
+               v-for="(item,index) in newData.data6" :index="index"
+               draggable="true"
+               @dragstart="onBigDragStart($event, index)"
+               @dragover.prevent="onBigDragOver($event, index)"
+               @drop="onBigDrop($event, index)"
+               @dragend="onDragEnd"
+          >
+            <p class="index-p">{{getLetter(index)}}</p>
+            <div class="min-editor-form-box">
+              <el-form-item label="" prop="data5" label-width="0">
+                <tinymce-container
+                  v-model="item.text"
+                  :height="300"
+                  :max-length="500"
+                  :upload-url="editorUploadUrl"
+                  :upload-headers="editorUploadHeaders"
+                />
+              </el-form-item>
+            </div>
+            <div class="editor-right-button-box">
+              <p class="el-icon-delete-solid" v-if="newData.data6[4]" @click="delItem(index)"></p>
+              <p class="el-icon-rank"></p>
+            </div>
+          </div>
+        </div>
+        <div class="title-content-box" v-if="newData.data1">
           <p></p>
           <p></p>
           <p>答案</p>
           <p>答案</p>
           <p></p>
           <p></p>
         </div>
         </div>
+        <div class="answers-content-box" v-if="newData.data1 == 1 || newData.data1 == 2">
+          <div class="for-answers-box" @click="checkAnswersButton(index)" v-for="(item,index) in newData.data7" :key="index" :class="item.checkItemType?'check-for-answers-box':''">
+            <p :class="item.checkItemType?'el-icon-circle-check':''"></p>
+            <p>{{getLetter(index)}}</p>
+          </div>
+        </div>
+        <div class="answers-content-box" v-if="newData.data1 == 3">
+          <div class="for-answers-box" @click="checkAnswersButton(index)" v-for="(item,index) in newData.data8" :key="index" :class="item.checkItemType?'check-for-answers-box':''">
+            <p :class="item.checkItemType?'el-icon-circle-check':''"></p>
+            <p>{{item.checkItemName}}</p>
+          </div>
+        </div>
         <div class="title-content-box">
         <div class="title-content-box">
           <p></p>
           <p></p>
           <p>解析</p>
           <p>解析</p>
           <p></p>
           <p></p>
         </div>
         </div>
+        <div class="editor-form-box">
+          <el-form-item label="" prop="data9" label-width="0">
+            <tinymce-container
+              v-model="newData.data9"
+              :height="300"
+              :max-length="500"
+              :upload-url="editorUploadUrl"
+              :upload-headers="editorUploadHeaders"
+            />
+          </el-form-item>
+        </div>
       </el-form>
       </el-form>
     </div>
     </div>
   </div>
   </div>
@@ -119,9 +170,9 @@
           data3:true,
           data3:true,
           data4:null,
           data4:null,
           data5:"",
           data5:"",
-          data6:[],
-          data7:[],
-          data8:"",
+          data6:[{text:''},{text:''},{text:''},{text:''}],
+          data7:[{checkItemType:false},{checkItemType:false},{checkItemType:false},{checkItemType:false}],
+          data8:[{checkItemName:'对',checkItemType:false},{checkItemName:'错',checkItemType:false}],
           data9:"",
           data9:"",
         },
         },
         // 编辑器图片/附件上传接口,按需修改
         // 编辑器图片/附件上传接口,按需修改
@@ -143,11 +194,6 @@
             { required: true, message: "请选择难度", validator: this.spaceJudgment, trigger: "blur" }
             { required: true, message: "请选择难度", validator: this.spaceJudgment, trigger: "blur" }
           ],
           ],
         },
         },
-
-        // 编辑器内容(v-model 绑定)
-        docContent: '<p>初始内容:这是一篇测试文档</p>',
-        // 是否禁用编辑器
-        isEditorDisabled: false
       }
       }
     },
     },
     created() {
     created() {
@@ -170,13 +216,150 @@
 
 
       },
       },
       // 提交按钮
       // 提交按钮
-      submitForm() {
+      submitButton() {
+        let self = this;
+        this.$refs["form"].validate(valid => {
+          if (valid) {
+            if(!self.newData.data5){
+              self.msgError('请输入题干')
+              return
+            }
+            if(this.newData.data1 == 1 ||this.newData.data1 == 2){
+              for(let i=0;i<self.newData.data6.length;i++){
+                if(!self.newData.data6[i].text){
+                  self.msgError('第'+(i+1)+'个选项没有填写内容.')
+                  return
+                }
+              }
+              let num = 0;
+              for(let i=0;i<self.newData.data7.length;i++){
+                if(self.newData.data7[i].checkItemType){
+                  num++
+                }
+              }
+              if(num == 0){
+                self.msgError('没有选择答案.')
+                return
+              }
+            }else if(this.newData.data1 == 3){
+              let num = 0;
+              for(let i=0;i<self.newData.data8.length;i++){
+                if(self.newData.data8[i].checkItemType){
+                  num++
+                }
+              }
+              if(num == 0){
+                self.msgError('没有选择答案.')
+                return
+              }
+            }
+          }
+        })
+      },
+      //添加选项
+      addItem(){
+        let self = this;
+        if(this.newData.data6[7]){
+          this.msgError('已达到数量上限,最多8个选项.')
+          return
+        }
+        this.$confirm('确认添加新选项?', "警告", {
+          confirmButtonText: "确认",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+        }).then(() => {
+          self.newData.data6.push({text:''});
+          self.newData.data7.push({checkItemType:false});
+        }).catch(() => {});
+      },
+      //删除选项
+      delItem(index){
+        let self = this;
+        this.$confirm('确认删除 '+this.getLetter(index)+' 选项?', "警告", {
+          confirmButtonText: "确认",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+        }).then(() => {
+          self.newData.data6.splice(index,1);
+          self.newData.data7.splice(index,1);
+        }).catch(() => {});
+      },
+      //答案选中
+      checkAnswersButton(index){
+        let self = this;
+        if(this.newData.data1 == 1){
+          if(self.newData.data7[index].checkItemType){
+            this.$set(self.newData.data7[index],'checkItemType',false);
+          }else{
+            for(let i=0;i<self.newData.data7.length;i++){
+              self.newData.data7[i].checkItemType = false;
+            }
+            this.$set(self.newData.data7[index],'checkItemType',true);
+          }
+        }else if(this.newData.data1 == 2){
+          this.$set(self.newData.data7[index],'checkItemType',!self.newData.data7[index].checkItemType);
+        }else if(this.newData.data1 == 3){
+          if(self.newData.data8[index].checkItemType){
+            this.$set(self.newData.data8[index],'checkItemType',false);
+          }else{
+            for(let i=0;i<self.newData.data8.length;i++){
+              self.newData.data8[i].checkItemType = false;
+            }
+            this.$set(self.newData.data8[index],'checkItemType',true);
+          }
+        }
+      },
+      //答案筛选器
+      answersScreening(){
+        let self = this;
+        if(this.newData.data1 == 1){
+          let num = 0;
+          let index = null;
+          for(let i=0;i<self.newData.data7.length;i++){
+            if(self.newData.data7[i].checkItemType && num == 0){
+              num++
+              index = i;
+            }
+          }
+          if (num != 0){
+            for(let i=0;i<self.newData.data7.length;i++){
+              self.newData.data7[i].checkItemType = false;
+            }
+            this.$set(self.newData.data7[index],'checkItemType',true);
+          }
+        }
+      },
+
+      // ========== 大项拖拽逻辑 ==========
+      onBigDragStart(e, index) {
+        this.dragInfo = { type: 'big', bigIndex: index, smallIndex: null };
+        // 给拖拽中的元素加个样式
+        e.target.classList.add('dragging');
+      },
+      onBigDragOver(e, index) {
+        // 只有当拖拽的是大项时才允许放置
+        if (this.dragInfo.type === 'big') {
+          e.currentTarget.classList.add('drag-over');
+        }
+      },
+      onBigDrop(e, targetIndex) {
+        if (this.dragInfo.type !== 'big') return;
+        const sourceIndex = this.dragInfo.bigIndex;
+        if (sourceIndex === targetIndex) return;
 
 
+        // 核心:交换数组元素
+        const movedItem = this.newData.data6.splice(sourceIndex, 1)[0];
+        this.newData.data6.splice(targetIndex, 0, movedItem);
+        const movedItem2 = this.newData.data7.splice(sourceIndex, 1)[0];
+        this.newData.data7.splice(targetIndex, 0, movedItem2);
       },
       },
-      // 接收编辑器传递的内容
-      handleGetContent(content) {
-        console.log('页面接收的编辑器内容:', content)
-        // 可在此处添加“保存到后端”等逻辑
+      // 拖拽结束,清理样式
+      onDragEnd(e) {
+        e.target.classList.remove('dragging');
+        document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
+        this.dragInfo = { type: null, bigIndex: null, smallIndex: null };
       },
       },
       //知识点
       //知识点
       examElKnowledgePointTreeList(){
       examElKnowledgePointTreeList(){
@@ -198,6 +381,15 @@
           }
           }
         });
         });
       },
       },
+      /**
+       * 将下标转换为字母
+       */
+      getLetter(index) {
+        if (typeof index !== 'number' || index < 0 || index >= 26) {
+          return '其他';
+        }
+        return String.fromCharCode(65 + index);
+      }
     },
     },
   }
   }
 </script>
 </script>
@@ -254,7 +446,6 @@
       flex: 1;
       flex: 1;
       display: flex;
       display: flex;
       flex-direction: column;
       flex-direction: column;
-      overflow: hidden;
       padding: 20px;
       padding: 20px;
       .top-content-box{
       .top-content-box{
         .flex-content-box{
         .flex-content-box{
@@ -266,7 +457,7 @@
         display: flex;
         display: flex;
         height:40px;
         height:40px;
         overflow: hidden;
         overflow: hidden;
-        margin:20px 40px;
+        margin:40px 40px;
         p:nth-child(1){
         p:nth-child(1){
           flex:1;
           flex:1;
           height:2px;
           height:2px;
@@ -286,9 +477,110 @@
           margin-top:19px;
           margin-top:19px;
         }
         }
       }
       }
+      .max-big-for-box{
+        position: relative;
+        .position-p{
+          position: absolute;
+          top:0;
+          right:40px;
+          width:100px;
+          line-height:40px;
+          border-radius:4px;
+          text-align: center;
+          color:#fff;
+          background-color: #0183FA;
+          cursor: pointer;
+        }
+        .for-editor-form-box{
+          display: flex;
+          margin:0 40px;
+          .index-p{
+            margin:0 10px 0 20px;
+            font-size:16px;
+            background-color: #0183FA;
+            color:#fff;
+            width:40px;
+            height:40px;
+            line-height:40px;
+            text-align: center;
+            border-radius:4px;
+          }
+          .min-editor-form-box{
+            flex:1;
+
+          }
+          .editor-right-button-box{
+            width:120px;
+            display: flex;
+            margin-top:80px;
+            p{
+              font-size:30px;
+              cursor: pointer;
+              margin-left:25px;
+            }
+            .el-icon-delete-solid{
+              color:#D81E06;
+            }
+            .el-icon-rank{
+              color:#999;
+            }
+          }
+        }
+      }
       .editor-form-box{
       .editor-form-box{
         margin:0 40px;
         margin:0 40px;
       }
       }
+      .answers-content-box{
+        display: flex;
+        margin:0 60px;
+        .for-answers-box{
+          display: flex;
+          border: 1px solid #999;
+          height:40px;
+          border-radius:4px;
+          margin-right:20px;
+          cursor: pointer;
+          p:nth-child(1){
+            width:16px;
+            height:16px;
+            border-radius:50%;
+            border:1px solid #999;
+            margin:11px 0 0 14px;
+          }
+          p:nth-child(2){
+            line-height:38px;
+            color:#999;
+            text-align: center;
+            width:40px;
+            font-size:16px;
+            margin-right:10px;
+          }
+        }
+        .check-for-answers-box{
+          border: 1px solid #0183FA;
+          p:nth-child(1){
+            margin:10px 0 0 14px;
+            border:none;
+            background-color: #fff;
+            color:#0183FA;
+            font-size:18px;
+          }
+          p:nth-child(2){
+            color:#0183FA;
+          }
+        }
+      }
+    }
+    .drag-handle {
+      cursor: move;
+      user-select: none; /* 防止拖拽时选中文字 */
+    }
+    .dragging {
+      opacity: 0.5;
+      background-color: #f5f7fa;
+    }
+    .drag-over {
+      border-top: 2px solid #409EFF !important; /* 放置位置的指示线 */
     }
     }
   }
   }
 </style>
 </style>