Browse Source

选择题判分增加多模版切换功能

dengshaobo 1 month ago
parent
commit
5c531899d7

+ 169 - 119
src/views/exam/components/ImagePreview.vue

@@ -9,11 +9,19 @@
     fullscreen 
     height="100%"
   >
-    <div class="preview_image">
+    <div 
+      class="preview_image" 
+      @wheel.prevent="HandleWheel"
+      @mousedown="HandleMouseDown"
+      @mousemove="HandleMouseMove"
+      @mouseup="HandleMouseUp"
+      @mouseleave="HandleMouseUp"
+    >
       <img 
         :src="currenImageUrl" 
         alt="" 
-        :style="{ transform: `scale(${zoomLevel}) rotate(${rotateAngle}deg)` }"
+        :style="imageStyle"
+        draggable="false"
       />
       
       <div class="preview_close" @click="CloseDialog()">
@@ -40,176 +48,218 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, watch, computed } from 'vue';
+import { ref, watch, computed } from 'vue'
 
+// ==================== 类型定义 ====================
 
+/** 图片项接口 */
 interface ImageItem {
-  url: string;
-  rotateAngle?: number;
-  pageNo?: number;
+  url: string
+  rotateAngle?: number
+  pageNo?: number
 }
 
+/** 组件 Props 接口 */
 interface Props {
-  modelValue: boolean;
-  imageList: ImageItem[];
-  index?: number;
-  showTool?: boolean;
+  modelValue: boolean
+  imageList: ImageItem[]
+  index?: number
 }
 
+/** 组件 Emits 接口 */
 interface Emits {
-  (e: 'update:modelValue', value: boolean): void;
-  (e: 'GetRotateAngle', data: { currentIndex: number; rotateAngle: number }): void;
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'GetRotateAngle', data: { currentIndex: number; rotateAngle: number }): void
 }
 
+// ==================== Props & Emits ====================
+
 const props = withDefaults(defineProps<Props>(), {
   imageList: () => [],
   index: 0,
-  showTool: false,
-});
-
-const emit = defineEmits<Emits>();
-
-const currenImageUrl = ref('');
-const currentIndex = ref(0);
-const zoomLevel = ref(1);
-const maxZoom = ref(3);
-const minZoom = ref(0.5);
-const zoomStep = ref(0.1);
-const rotateAngle = ref(0);
-
-const toolList = [
-  {
-    title: '放大',
-    icon: 'icon_fangda',
-    value: '1',
-  },
-  {
-    title: '缩小',
-    icon: 'icon_suoxiao',
-    value: '2',
-  },
-  {
-    title: '逆时针90°',
-    icon: 'icon_nishizhen90',
-    value: '5',
-  },
-  {
-    title: '顺时针90°',
-    icon: 'icon_shunshizhen90',
-    value: '6',
-  },
-];
+})
+
+const emit = defineEmits<Emits>()
 
+// ==================== 响应式状态 ====================
+
+/** 当前图片URL */
+const currenImageUrl = ref('')
+/** 当前图片索引 */
+const currentIndex = ref(0)
+/** 缩放比例 */
+const zoomLevel = ref(1)
+/** 最大缩放比例 */
+const maxZoom = 3
+/** 最小缩放比例 */
+const minZoom = 0.5
+/** 缩放步进值 */
+const zoomStep = 0.1
+/** 旋转角度 */
+const rotateAngle = ref(0)
+
+/** 拖拽相关状态 */
+const translateX = ref(0)
+const translateY = ref(0)
+const isDragging = ref(false)
+const startX = ref(0)
+const startY = ref(0)
+
+// ==================== 计算属性 ====================
+
+/** 弹窗显示状态(双向绑定) */
 const showDialog = computed({
   get() {
-    return props.modelValue;
+    return props.modelValue
   },
   set(val) {
-    emit('update:modelValue', val);
+    emit('update:modelValue', val)
   },
-});
+})
+
+/** 图片内联样式(缩放 + 旋转 + 拖拽位移) */
+const imageStyle = computed(() => ({
+  transform: `translate(${translateX.value}px, ${translateY.value}px) scale(${zoomLevel.value}) rotate(${rotateAngle.value}deg)`,
+  transition: isDragging.value ? 'none' : 'transform 0.15s',
+  cursor: isDragging.value ? 'grabbing' : 'grab',
+}))
+
+// ==================== 监听器 ====================
 
+/** 监听弹窗打开,加载当前索引图片信息 */
 watch(
   () => props.modelValue,
   (newVal) => {
     if (newVal && props.imageList.length > 0) {
-      const targetIndex = props.index || 0;
-      currenImageUrl.value = props.imageList[targetIndex].url;
-      rotateAngle.value = props.imageList[targetIndex].rotateAngle || 0;
-      currentIndex.value = targetIndex;
+      LoadImageByIndex(props.index || 0)
     }
   }
-);
+)
 
-const CloseDialog = () => {
-  showDialog.value = false;
-  let obj = {
-    currentIndex: currentIndex.value,
-    rotateAngle: rotateAngle.value,
-  };
-  emit('GetRotateAngle', obj);
-};
+// ==================== 图片切换 ====================
 
-const GetReturn = () => {
-  let obj = {
-    currentIndex: currentIndex.value,
-    rotateAngle: rotateAngle.value,
-  };
-  emit('GetRotateAngle', obj);
-};
+/**
+ * 根据索引加载图片信息
+ * @param targetIndex 目标图片索引
+ */
+const LoadImageByIndex = (targetIndex: number) => {
+  currenImageUrl.value = props.imageList[targetIndex].url
+  rotateAngle.value = props.imageList[targetIndex].rotateAngle || 0
+  currentIndex.value = targetIndex
+  ResetTransform()
+}
 
+/** 重置缩放和位移状态 */
+const ResetTransform = () => {
+  zoomLevel.value = 1
+  translateX.value = 0
+  translateY.value = 0
+}
+
+/** 切换到上一张图片 */
 const LastImage = () => {
   if (currentIndex.value > 0) {
-    currentIndex.value--;
+    LoadImageByIndex(currentIndex.value - 1)
   }
-  currenImageUrl.value = props.imageList[currentIndex.value].url;
-  rotateAngle.value = props.imageList[currentIndex.value].rotateAngle || 0;
-};
+}
 
+/** 切换到下一张图片 */
 const NextImage = () => {
   if (currentIndex.value < props.imageList.length - 1) {
-    currentIndex.value++;
+    LoadImageByIndex(currentIndex.value + 1)
   }
-  currenImageUrl.value = props.imageList[currentIndex.value].url;
-  rotateAngle.value = props.imageList[currentIndex.value].rotateAngle || 0;
-};
+}
 
-const ToolClick = (item: any) => {
-  if (item.value === '1') {
-    ZoomIn();
-  }
-  if (item.value === '2') {
-    ZoomOut();
-  }
-  if (item.value === '5') {
-    RotateCounterClockwise90();
-  }
-  if (item.value === '6') {
-    RotateClockwise90();
+// ==================== 弹窗控制 ====================
+
+/** 关闭弹窗并回传旋转角度信息 */
+const CloseDialog = () => {
+  showDialog.value = false
+  emit('GetRotateAngle', {
+    currentIndex: currentIndex.value,
+    rotateAngle: rotateAngle.value,
+  })
+}
+
+// ==================== 鼠标滚轮缩放 ====================
+
+/**
+ * 滚轮事件处理:向上滚放大,向下滚缩小
+ * @param e 滚轮事件对象
+ */
+const HandleWheel = (e: WheelEvent) => {
+  if (e.deltaY < 0) {
+    ZoomIn()
+  } else {
+    ZoomOut()
   }
-};
+}
 
+/** 放大图片(最大 3x) */
 const ZoomIn = () => {
-  if (zoomLevel.value < maxZoom.value) {
-    zoomLevel.value += zoomStep.value;
+  if (zoomLevel.value < maxZoom) {
+    zoomLevel.value = Math.round((zoomLevel.value + zoomStep) * 100) / 100
   }
-};
+}
 
+/** 缩小图片(最小 0.5x) */
 const ZoomOut = () => {
-  if (zoomLevel.value > minZoom.value) {
-    zoomLevel.value -= zoomStep.value;
+  if (zoomLevel.value > minZoom) {
+    zoomLevel.value = Math.round((zoomLevel.value - zoomStep) * 100) / 100
   }
-};
+}
+
+// ==================== 鼠标拖拽移动 ====================
 
+/**
+ * 鼠标按下:记录起始位置,进入拖拽状态
+ * @param e 鼠标事件对象
+ */
+const HandleMouseDown = (e: MouseEvent) => {
+  // 仅左键触发拖拽
+  if (e.button !== 0) return
+  isDragging.value = true
+  startX.value = e.clientX - translateX.value
+  startY.value = e.clientY - translateY.value
+}
+
+/**
+ * 鼠标移动:计算拖拽偏移量,更新图片位置
+ * @param e 鼠标事件对象
+ */
+const HandleMouseMove = (e: MouseEvent) => {
+  if (!isDragging.value) return
+  translateX.value = e.clientX - startX.value
+  translateY.value = e.clientY - startY.value
+}
+
+/** 鼠标松开 / 移出区域:结束拖拽 */
+const HandleMouseUp = () => {
+  isDragging.value = false
+}
+
+// ==================== 旋转 ====================
+
+/** 逆时针旋转90度 */
 const RotateCounterClockwise90 = () => {
-  rotateAngle.value -= 90;
-  GetReturn();
-};
+  rotateAngle.value -= 90
+}
 
+/** 顺时针旋转90度 */
 const RotateClockwise90 = () => {
-  rotateAngle.value += 90;
-  GetReturn();
-};
+  rotateAngle.value += 90
+}
 </script>
 
 <style lang="scss" scoped>
-.preview_canvas {
-  width: 100%;
-  height: 100%;
-  .main_container {
-    width: 100%;
-    height: 100%;
-    position: relative;
-    background: transparent;
-    user-select: none;
-    overflow: hidden;
-    display: flex;
-    justify-content: center;
-    align-items: center;
+.preview_image {
+  overflow: hidden;
 
-    .paper_template {
-      position: absolute;
-    }
+  img {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: contain;
+    user-select: none;
   }
 }
 </style>

+ 110 - 79
src/views/exam/components/chooseTemplate.vue

@@ -2,28 +2,35 @@
   <el-dialog
     v-model="dialogVisible"
     title="请选择机读卡模版"
-    width="580px"
+    width="800px"
     class="page_dialog"
     :before-close="handleClose"
   >
     <div class="table_42">
-      <el-table  :data="tableData"    stripe style="width: 100%" >
-        
-        <el-table-column label=""  width="80">
+      <el-table
+        :data="tableData"
+        stripe
+        style="width: 100%"
+        highlight-current-row
+        @row-click="handleRowClick"
+      >
+        <el-table-column label="" width="80">
           <template v-slot="scope">
-            <el-radio  v-model="scope.row.selected"></el-radio>
+            <el-radio
+              v-model="selectedTemplateId"
+              :label="scope.row.id"
+            >&nbsp;</el-radio>
           </template>
         </el-table-column>
-        <el-table-column  prop="templateCode"  label="模板编号" ></el-table-column>
-        <el-table-column  prop="templateName"  label="模板名称"></el-table-column>
-        <el-table-column  prop="option"  label="操作">
+        <el-table-column prop="templateCode" label="模板编号" width="100"></el-table-column>
+        <el-table-column prop="templateName" label="模板名称"></el-table-column>
+        <el-table-column prop="option" label="操作" width="100">
           <template v-slot="scope">
-            <div class="ele_button table_row_button">
+            <div class="ele_button table_row_button" @click.stop="OpenPaperView(scope.row)">
               <span class="btn_editor">预览</span>
-            </div> 
+            </div>
           </template>
         </el-table-column>
-       
       </el-table>
     </div>
     <template #footer>
@@ -33,28 +40,28 @@
       </span>
     </template>
   </el-dialog>
+  <!-- 模板预览弹窗 -->
+  <ImagePreview v-model="showPreview" :imageList="previewImageList"></ImagePreview>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive, computed, watch,onMounted } from 'vue'
-import type { ElTableColumn, FormInstance, FormRules } from 'element-plus'
+import { ref, computed, watch } from 'vue'
 import { ElMessage } from 'element-plus'
-
-// 1. 确保导入接口函数,请根据实际路径调整
-import { getTemplateList,useTemplate } from '@/api/exam' 
+import { getTemplateList, useTemplate } from '@/api/exam'
 import { useExamStore } from '@/store/exam'
-
+import ImagePreview from './ImagePreview.vue'
 // 实例化 Store
 const examStore = useExamStore()
-// 定义 Props 和 Emits
-const props = defineProps<{
-  modelValue: boolean,
 
+// 定义 Props
+const props = defineProps<{
+  modelValue: boolean
+  currentTemplateId?: string | number // 当前使用的模板ID,用于回显
 }>()
 
 const emit = defineEmits<{
   (e: 'update:modelValue', value: boolean): void
-  (e: 'success'): void
+  (e: 'success', templateData: any): void
 }>()
 
 // 弹窗显示状态的双向绑定
@@ -63,91 +70,115 @@ const dialogVisible = computed({
   set: (val) => emit('update:modelValue', val)
 })
 
-// 表单引用
-const formRef = ref<FormInstance>()
+// 表格数据
+const tableData = ref<any[]>([])
+// 当前选中的模板ID
+const selectedTemplateId = ref<string | number>()
+// 加载状态
+const loading = ref(false)
 
-//定义用于存储下拉菜单选项的响应式数据
-const tableData=ref<any[]>([]);
-
-const selectedId=ref<any>();//选中的模板id
-
-const loading=ref(false);//加载状态
+// 模板预览相关状态
+const showPreview = ref(false)
+const previewImageList = ref<any[]>([])
 
 // 考试科目 ID
 const examSubjectId = computed(() => {
-  // 替换为: return makeTemplateStore.examTempDetail?.id
   return examStore.currentExam?.id
-})//计算属性
+})
 
 // 关闭弹窗前的处理
 const handleClose = (done: () => void) => {
-  // 可以在这里添加确认关闭的逻辑
   done()
 }
 
-onMounted(()=>{
-  GetTemplateList();
-})
-
-// 获取模板列表
-const GetTemplateList=async()=>{
-  let params={
-    pageNum:1,
-    pageSize:1000,
-  };
-  const res:any = await getTemplateList(params);
-  console.log("打印获取的结果",res)
-  if(res.code==200 && res.data)
-  {
-    tableData.value=res.data.records;
-    console.log("打印formData",tableData.value)
-    selectedId.value=tableData.value[0].id;
+// 点击表格行选中模板
+const handleRowClick = (row: any) => {
+  selectedTemplateId.value = row.id
+}
 
+// 打开预览窗口:获取模板详情并展示机读卡预览图
+const OpenPaperView = async (row: any) => {
+  try {
+      console.log('预览模板', row)
+      previewImageList.value = row.pages.map((item: any) => ({
+        url: item.templateUrl,
+        pageNo: item.pageNo,
+        rotateAngle: 0,
+      }))
+      showPreview.value = true
    
+  } catch (error) {
+    console.error('获取模板预览失败', error)
+    ElMessage.error('获取模板预览失败,请稍后重试')
+  }
+}
+
+// 获取模板列表
+const GetTemplateList = async () => {
+  const params = {
+    pageNum: 1,
+    pageSize: 1000,
+  }
+  const res: any = await getTemplateList(params)
+  if (res.code == 200 && res.data) {
+    tableData.value = res.data.records
+    // 回显当前模板:优先使用 props 传入的 currentTemplateId,否则默认选中第一个
+    if (props.currentTemplateId) {
+      selectedTemplateId.value = props.currentTemplateId
+    } else if (tableData.value.length > 0) {
+      selectedTemplateId.value = tableData.value[0].id
+    }
   }
 }
-//监听弹窗打开,调用接口
-watch(dialogVisible, (val) => { 
-  if (val) 
-  {
-    //弹窗打开时获取数据
+
+// 监听弹窗打开,调用接口
+watch(dialogVisible, (val) => {
+  if (val) {
     GetTemplateList()
   }
 })
 
-
-
-
 // 取消按钮
 const HandleCancel = () => {
   dialogVisible.value = false
-
 }
 
 // 确定按钮
-const HandleSubmit= () => { 
-  console.log("确定选择模板")
-
-  console.log("examSubjectId",examSubjectId.value)
-  console.log("selectedId",selectedId.value)
-  let params={
-    examSubjectId:examSubjectId.value,//考试科目id
-    templateId:selectedId.value,//模板id
-  };
-  useTemplate(params).then(res=>{ 
-    console.log("打印模板接口返回res",res)
-    if(res.code==200)
-    {
-      ElMessage.success("使用成功");
-      dialogVisible.value = false
-      emit('success')
-    }
-  })
-}
-
+const HandleSubmit = () => {
+  if (!selectedTemplateId.value) {
+    ElMessage.warning('请选择一个模板')
+    return
+  }
+  // 如果选中的是当前模板,无需更换
+  if (selectedTemplateId.value === props.currentTemplateId) {
+    ElMessage.info('当前模板未变更')
+    dialogVisible.value = false
+    return
+  }
 
+  loading.value = true
+  const params = {
+    examSubjectId: examSubjectId.value,
+    templateId: selectedTemplateId.value,
+  }
+  useTemplate(params)
+    .then((res: any) => {
+      if (res.code == 200) {
+        // 找到选中的模板完整数据
+        const selectedTemplate = tableData.value.find(
+          (item) => item.id === selectedTemplateId.value
+        )
+        ElMessage.success('更换模板成功')
+        dialogVisible.value = false
+        // 将选中的模板数据回传给父组件
+        emit('success', selectedTemplate)
+      }
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
 </script>
 
 <style lang="scss" scoped>
-
 </style>

+ 49 - 8
src/views/exam/question.vue

@@ -8,8 +8,11 @@
             <!-- <el-button style="margin-left: 10px;width: 116px;">引用试题结构</el-button> -->
         </div>
         <div class="choose_template">
-            当前模板:{{templateName}}  
-             <!-- <span class="choose_btn" @click="OpenChooseTemplate()">更换</span> -->
+            当前模板:{{currentTemplateName}}  
+            <!-- <el-select  v-model="queryParams.start"   placeholder="请选择"  class="select_width" >
+                <el-option :label="number" :value="number" v-for="number in templateQuestionCount"></el-option>
+            </el-select> -->
+             <span class="choose_btn" @click="OpenChooseTemplate()">更换</span>
         </div>
     </div>
     <div class="page_jg_20"></div>
@@ -20,11 +23,11 @@
             <div class="item_search">
                 <div class="search_name">题目从</div>
                 <el-select  v-model="queryParams.start"   placeholder="请选择"  class="select_width" >
-                    <el-option :label="number" :value="number" v-for="number in templateQuestionCount"></el-option>
+                    <el-option :label="number" :value="number" v-for="number in questionRange"></el-option>
                 </el-select>
                 <div class="search_name">至</div>
                 <el-select  v-model="queryParams.end"   placeholder="请选择"  class="select_width" >
-                    <el-option :label="number" :value="number" v-for="number in templateQuestionCount"></el-option>
+                    <el-option :label="number" :value="number" v-for="number in questionRange"></el-option>
                 </el-select>
             </div>
             <div class="item_search">
@@ -140,7 +143,7 @@
             <el-button type="primary"  style="width: 200px;" @click="FinishQuestion()">完成</el-button>
         </div>
     </div>
-    <ChooseTemplate v-model="showChooseTemplate"></ChooseTemplate>
+    <ChooseTemplate v-model="showChooseTemplate" :currentTemplateId="currentTemplateId" @success="onTemplateChanged"></ChooseTemplate>
     <EditorQuestion ref="enterStandardEdit" @AnswerEnter="EditorAnswerEnter"></EditorQuestion>
     <PaperPreview v-model="showPaperDialog" :imageList="previewImageList" :showDraw="true" :answerData="previewAnswerData"></PaperPreview>
   </div>
@@ -190,9 +193,13 @@ const queryParams= ref({
 //多选题「少选得分」(questionType==2 时使用),与模板 v-model="shaoxuan" 绑定
 const shaoxuan = ref('')//批量设置多选题给分分布的功能
 
-const templateName=ref('机读卡');
+
 const scanText=ref('开始扫描');
 const templateQuestionCount=ref(0);//模板题目总数量
+const templateQuestionStart=ref(1);//模板题目起始编号
+const templateQuestionEnd=ref(0);//模板题目结束编号
+const currentTemplateId=ref(0);//当前模板ID
+const currentTemplateName=ref('');//当前模板名称
 const showChooseTemplate=ref(false);// 弹出选择模板的弹窗
 const tableData = ref<QuestionItem[]>([])
 const tableLoading = ref(false)//表格loading状态
@@ -229,6 +236,18 @@ const templateId=computed(() => {
   return examStore.currentExam?.templateId
 })//计算属性
 
+// 题目编号可选范围(从接口返回的 start 到 end)
+const questionRange=computed(() => {
+    const start = Number(templateQuestionStart.value) || 1
+    const end = Number(templateQuestionEnd.value) || 0
+    if (end < start) return []
+    const list: number[] = []
+    for (let i = start; i <= end; i++) {
+        list.push(i)
+    }
+    return list
+})
+
 const totalScore=computed(() => {
   if (!tableData.value || tableData.value.length === 0) {
     return 0;
@@ -298,6 +317,22 @@ const OpenChooseTemplate=()=>{
     showChooseTemplate.value=true;
 }
 
+//模板更换成功回调
+const onTemplateChanged=(templateData: any)=>{
+    console.log('模板更换成功', templateData)
+    currentTemplateId.value = templateData.id
+    currentTemplateName.value = templateData.templateName
+    templateQuestionCount.value = templateData.questionEnd || 0
+    templateQuestionStart.value = templateData.questionStart || 1
+    templateQuestionEnd.value = templateData.questionEnd || 0
+    // 更新 store 中的 templateId
+    if (examStore.currentExam) {
+        examStore.currentExam.templateId = templateData.id
+    }
+    // 重新获取试题列表
+    GetSmartQuestionList()
+}
+
 //切换题目类型
 const TypeChange=(item:any)=>{
     console.log("打印题目类型",item)
@@ -739,8 +774,14 @@ const GetTemplateInfo=()=>{
         console.log("打印获取模板信息结果",res)
         if(res.code==200)
         {
-            templateQuestionCount.value=res.data.totalQuestionSize;//题目数量
-            templateName.value=res.data.templateName;
+            templateQuestionCount.value=res.data.questionEnd;//题目数量
+            templateQuestionStart.value=res.data.questionStart;//题目起始编号
+            templateQuestionEnd.value=res.data.questionEnd;//题目结束编号
+            queryParams.value.end=res.data.questionEnd;//题目结束
+            queryParams.value.start=res.data.questionStart;//题目开始
+            currentTemplateName.value=res.data.templateName;//当前模板名称
+            currentTemplateId.value=res.data.id;//当前模板ID
+            
         }
     })
 }