浏览代码

导入扫描图片上传更新

dengshaobo 1 月之前
父节点
当前提交
22f0660975

+ 2 - 1
src/main.ts

@@ -7,6 +7,7 @@ import ElementPlus from 'element-plus'//引入elmplus
 import 'element-plus/dist/index.css'
 import zhCn from 'element-plus/es/locale/lang/zh-cn'  // 导入中文语言包
 import * as ElIcons from '@element-plus/icons-vue'
+import draggable from '@/utils/derective/draggable';
 
 
 import './style.css'
@@ -17,7 +18,7 @@ const pinia = createPinia() // 创建实例
 Object.keys(ElIcons).forEach((key) => {
   app.component(key, (ElIcons as Record<string, any>)[key])
 })//ele icon
-
+app.directive('draggable', draggable);
 app.use(ElementPlus, { locale: zhCn })
 app.use(pinia) // 使用 Pinia vue3 的状态管理 替代vuex
 app.use(router) // 注册路由

+ 279 - 0
src/styles/common.scss

@@ -1688,6 +1688,187 @@ body {
       }
     }
 
+    //导入扫描图片公共样式
+    //重新上传的图片列表
+    .center_pic_list {
+      width: 100%;
+      // min-height: 562px;
+      height: auto;
+      max-height: 400px;
+      overflow-y: auto;
+      overflow-x: hidden;
+      margin-top: 20px;
+
+      .list_img_content {
+        display: flex;
+        justify-content: flex-start;
+        align-items: flex-start;
+        /* 顶部对齐 */
+        box-sizing: border-box;
+        flex-wrap: wrap;
+        gap: 16px;
+
+        .list_img_item {
+          /* 基础宽度 - 每行7个 */
+          flex: 0 0 calc((100% - 16px * 4) / 5);
+
+          /* 最小宽度 */
+          min-width: 160px;
+          /* 保持宽高比(可选) */
+          // aspect-ratio: 1/1;
+          // margin-right: 15px;
+          // margin-bottom: 16px;
+          /* 内容样式 */
+
+          height: auto;
+
+          // margin-right: 13px;
+          .item_batch_name {
+            width: 100%;
+            font-size: 14px;
+            color: #666666;
+            text-align: left;
+
+            display: flex;
+            justify-content: space-between;
+            line-height: 30px;
+
+            .name_abnormal {
+              color: #F56C6C;
+            }
+          }
+
+          .item_batch_img {
+            width: 100%;
+            height: auto;
+
+            padding: 8px;
+            box-sizing: border-box;
+            background-color: #F0F4F8;
+            border-radius: 6px 6px 6px 6px;
+            border: 1px solid #EBEEF5;
+            display: flex;
+            justify-content: center;
+            position: relative;
+
+            img {
+              width: 100%;
+              height: auto;
+              object-fit: cover;
+              z-index: 9;
+            }
+
+          }
+
+          .item_batch_img_error {
+            border: 1px solid #F56C6C !important;
+          }
+
+          .item_batch_img:hover .button_group {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+          }
+
+          .button_group {
+            position: absolute;
+
+            width: 100%;
+            height: 100%;
+            z-index: 10;
+            top: 0px;
+            left: 0;
+            background: rgba(0, 0, 0, 0.4);
+            display: none;
+            align-items: center;
+            justify-content: center;
+            gap: 16px;
+            z-index: 999;
+
+            .pic_button_view {
+              width: 68px;
+              height: 34px;
+              text-align: center;
+              line-height: 34px;
+
+              cursor: pointer;
+              background-color: rgba(255, 255, 255, 1);
+              border: 1px solid rgba(220, 223, 230, 1);
+              border-radius: 4px;
+              color: #666666;
+
+              i {
+                margin-right: 5px;
+              }
+
+              font-size: 14px;
+            }
+
+
+          }
+
+          .item_batch_progress {
+            position: absolute;
+            width: 145px;
+            height: 40px;
+
+            left: calc(50% - 72px);
+            top: calc(50% - 20px);
+            z-index: 999;
+            text-align: center;
+            color: #2E64FA;
+            font-size: 16px;
+            font-weight: 400;
+          }
+
+          .item_batch_failed {
+            position: absolute;
+            width: 145px;
+            height: 40px;
+
+            left: calc(50% - 72px);
+            top: calc(50% - 20px);
+            z-index: 999;
+            text-align: center;
+            color: #F56C6C;
+            font-size: 16px;
+            font-weight: 400;
+            line-height: 40px;
+          }
+        }
+
+        // @media (max-width: 1458px) {
+        //     .list_img_item
+        //     {
+        //        flex: 0 0 calc((100% - 16px * 5) / 6);
+        //     }
+
+        // }
+        // @media (max-width: 1280px) {
+        //     .list_img_item
+        //     {
+        //        flex: 0 0 calc((100% - 16px * 4) / 5);
+        //     }
+
+        // }
+        // @media (max-width: 1109px) {
+        //     .list_img_item
+        //     {
+        //        flex: 0 0 calc((100% - 16px * 3) / 4);
+        //     }
+
+        // }
+        // @media (max-width: 940px) {
+        //     .list_img_item
+        //     {
+        //        flex: 0 0 calc((100% - 16px * 2) / 3);
+        //     }
+
+        // }
+
+
+      }
+    }
 
     //重新识别公共样式
     .re_identify {
@@ -5598,3 +5779,101 @@ body {
   color: #2e64fa;
   cursor: pointer;
 }
+
+
+.dialog_group {
+    width: 100%;
+    margin: auto;
+    padding-left: 6px;
+
+    .radio_title {
+      font-weight: 400;
+      font-size: 16px;
+      color: #666666;
+      // background-color: red;
+      display: inline-block;
+      width: 87px;
+    }
+
+    .el-radio__label {
+      font-size: 16px;
+      font-weight: 400;
+    }
+
+    .radio_group_socre {
+      width: 100%;
+      height: auto;
+      line-height: 50px;
+      font-weight: 400;
+      font-size: 16px;
+      color: #666666;
+      padding-left: 24px;
+      display: flex;
+      justify-content: flex-start;
+      margin-top: 16px;
+      margin-bottom: 10px;
+
+      .el-input {
+        width: 52px !important;
+        height: 30x !important;
+        padding: 0px !important;
+        // margin-left: 5px;
+        margin-right: 5px;
+      }
+
+      .el-input__inner {
+        width: 52px !important;
+        height: 30px !important;
+        padding: 0px !important;
+        text-align: center;
+      }
+
+      .el-radio__input.is-checked+.el-radio__label {
+        font-weight: 400;
+        font-size: 16px;
+        color: #666;
+      }
+
+      .el-select .el-select__wrapper, .el-input .el-input__wrapper, .el-date-editor.el-input, .el-button
+      {
+        height: 30px !important;
+      }
+    }
+
+    .radio_group_long_input {
+      line-height: 50px;
+      color: #666;
+      padding-left: 24px;
+
+      .input_long_100 {
+        .el-input {
+          width: 100% !important;
+          height: 36px !important;
+          padding: 0px !important;
+          // margin-left: 5px;
+          margin-right: 5px;
+        }
+      }
+
+      .el-input {
+        width: 600px !important;
+        height: 36px !important;
+        padding: 0px !important;
+        // margin-left: 5px;
+        margin-right: 5px;
+      }
+
+      .el-input__inner {
+        width: 100% !important;
+        height: 36px !important;
+        padding: 10px !important;
+        text-align: left;
+      }
+    }
+}
+
+
+.el-button.is-disabled, .el-button.is-disabled:hover
+{
+  background-color: #97b2fd;
+}

+ 181 - 0
src/utils/derective/draggable.ts

@@ -0,0 +1,181 @@
+import type { Directive, DirectiveBinding } from 'vue';
+
+interface Position {
+  x: number;
+  y: number;
+}
+
+interface DragCallbackParams {
+  x: number;
+  y: number;
+}
+
+interface DragBindingValue {
+  onDrag?: (params: DragCallbackParams) => void;
+  onStop?: (params: DragCallbackParams) => void;
+}
+
+interface DraggableElement extends HTMLElement {
+  _mousedownHandler?: (e: MouseEvent) => void;
+  _touchstartHandler?: (e: TouchEvent) => void;
+  _mousemoveHandler?: (e: MouseEvent) => void;
+  _mouseupHandler?: (e: MouseEvent) => void;
+  _touchmoveHandler?: (e: TouchEvent) => void;
+  _touchendHandler?: (e: TouchEvent) => void;
+}
+
+const draggable: Directive<DraggableElement, DragBindingValue> = {
+  mounted(el: DraggableElement, binding: DirectiveBinding<DragBindingValue>) {
+    let startPosition: Position = { x: 0, y: 0 };
+    let dialogPosition: Position = { x: 0, y: 0 };
+    let dragging = false;
+    let isClick = false;
+
+    const handleStart = (x: number, y: number) => {
+      startPosition.x = x;
+      startPosition.y = y;
+      // 获取当前元素的位置,如果之前没有设置过则默认为0
+      const style = window.getComputedStyle(el);
+      dialogPosition.x = parseInt(style.left, 10) || 0;
+      dialogPosition.y = parseInt(style.top, 10) || 0;
+      dragging = false;
+      isClick = true;
+    };
+
+    const handleMove = (x: number, y: number) => {
+      if (!dragging) {
+        const moveDistance = Math.sqrt(
+          Math.pow(x - startPosition.x, 2) + 
+          Math.pow(y - startPosition.y, 2)
+        );
+        
+        // 移动距离超过5px视为拖拽
+        if (moveDistance > 5) {
+          dragging = true;
+          isClick = false;
+        }
+      }
+      
+      if (dragging) {
+        const offsetX = x - startPosition.x;
+        const offsetY = y - startPosition.y;
+        el.style.left = `${dialogPosition.x + offsetX}px`;
+        el.style.top = `${dialogPosition.y + offsetY}px`;
+        
+        // 触发拖拽中回调
+        if (binding.value && typeof binding.value === 'object' && binding.value.onDrag) {
+          binding.value.onDrag({
+            x: dialogPosition.x + offsetX,
+            y: dialogPosition.y + offsetY
+          });
+        }
+      }
+    };
+
+    const handleEnd = (e: Event) => {
+      // 触发停止回调
+      if (binding.value && typeof binding.value === 'object' && binding.value.onStop) {
+        const finalLeft = parseInt(el.style.left, 10) || 0;
+        const finalTop = parseInt(el.style.top, 10) || 0;
+        binding.value.onStop({
+          x: finalLeft,
+          y: finalTop
+        });
+      }
+      
+      dragging = false;
+      isClick = false;
+    };
+
+    // 鼠标事件处理函数
+    const handleMouseDown = (e: MouseEvent) => {
+      e.stopPropagation();
+      // 只有点击在标题栏或特定区域才允许拖拽,这里假设整个el都可以拖拽
+      // 如果需要限制拖拽手柄,可以判断 e.target
+      handleStart(e.clientX, e.clientY);
+      document.addEventListener('mousemove', handleMouseMove);
+      document.addEventListener('mouseup', handleMouseUp);
+    };
+
+    const handleMouseMove = (e: MouseEvent) => {
+      handleMove(e.clientX, e.clientY);
+      if (dragging) {
+        e.preventDefault();
+        e.stopPropagation();
+      }
+    };
+
+    const handleMouseUp = (e: MouseEvent) => {
+      document.removeEventListener('mousemove', handleMouseMove);
+      document.removeEventListener('mouseup', handleMouseUp);
+      handleEnd(e);
+    };
+
+    // 触摸事件处理函数
+    const handleTouchStart = (e: TouchEvent) => {
+      if (e.touches.length > 0) {
+        e.stopPropagation();
+        const touch = e.touches[0];
+        handleStart(touch.clientX, touch.clientY);
+        document.addEventListener('touchmove', handleTouchMove, { passive: false });
+        document.addEventListener('touchend', handleTouchEnd);
+      }
+    };
+
+    const handleTouchMove = (e: TouchEvent) => {
+      if (e.touches.length > 0) {
+        e.preventDefault(); // 阻止页面滚动
+        e.stopPropagation();
+        const touch = e.touches[0];
+        handleMove(touch.clientX, touch.clientY);
+      }
+    };
+
+    const handleTouchEnd = (e: TouchEvent) => {
+      document.removeEventListener('touchmove', handleTouchMove);
+      document.removeEventListener('touchend', handleTouchEnd);
+      handleEnd(e);
+    };
+
+    // 绑定事件并保存引用以便卸载时清理
+    el._mousedownHandler = handleMouseDown;
+    el._touchstartHandler = handleTouchStart;
+    el._mousemoveHandler = handleMouseMove;
+    el._mouseupHandler = handleMouseUp;
+    el._touchmoveHandler = handleTouchMove;
+    el._touchendHandler = handleTouchEnd;
+
+    el.addEventListener('mousedown', handleMouseDown);
+    el.addEventListener('touchstart', handleTouchStart, { passive: false });
+  },
+  
+  beforeUnmount(el: DraggableElement) {
+    // 清理事件监听器,防止内存泄漏
+    if (el._mousedownHandler) {
+      el.removeEventListener('mousedown', el._mousedownHandler);
+      delete el._mousedownHandler;
+    }
+    if (el._touchstartHandler) {
+      el.removeEventListener('touchstart', el._touchstartHandler);
+      delete el._touchstartHandler;
+    }
+    if (el._mousemoveHandler) {
+      document.removeEventListener('mousemove', el._mousemoveHandler);
+      delete el._mousemoveHandler;
+    }
+    if (el._mouseupHandler) {
+      document.removeEventListener('mouseup', el._mouseupHandler);
+      delete el._mouseupHandler;
+    }
+    if (el._touchmoveHandler) {
+      document.removeEventListener('touchmove', el._touchmoveHandler);
+      delete el._touchmoveHandler;
+    }
+    if (el._touchendHandler) {
+      document.removeEventListener('touchend', el._touchendHandler);
+      delete el._touchendHandler;
+    }
+  }
+};
+
+export default draggable;

+ 298 - 0
src/views/exam/components/EditorQuestion.vue

@@ -0,0 +1,298 @@
+<template>
+    <div class="page_dialog">
+        <el-dialog 
+            :title="title" 
+            v-model="dialogVisible" 
+            :close-on-click-modal="false"
+            :width="answerOption.questionType == 2 ? '850px' : '480px'" 
+            class="page_dialog"
+        >
+            <div class="padding_20" style="padding-bottom: 10px;">
+                <div class="dialog_center">
+                    <el-form ref="formRef" :model="answerOption" label-width="120px">
+                        <el-form-item label="题目名称:">
+                            <el-input v-model="answerOption.questionName" placeholder="请输入"></el-input>
+                        </el-form-item>
+                        <el-form-item label="题目类型:">
+                            <el-select 
+                                :popper-append-to-body="false" 
+                                v-model="answerOption.questionType" 
+                                @change="TypeChange"
+                            >
+                                <el-option :value="1" label="单选题"></el-option>
+                                <el-option :value="2" label="多选题"></el-option>
+                                <el-option :value="3" label="判断题" v-if="answerOption.optionsSize == 2"></el-option>
+                            </el-select>
+                        </el-form-item>
+                        
+                        <!-- 单选题标准答案 -->
+                        <el-form-item label="标准答案:" v-if="answerOption.questionType == 1">
+                            <el-select v-model="answerOption.standardAnswer" placeholder="请选择">
+                                <el-option 
+                                    :value="answerList[index]" 
+                                    :label="answerList[index]"
+                                    v-for="(item, index) in answerOption.optionsSize" 
+                                    :key="index"
+                                ></el-option>
+                            </el-select>
+                        </el-form-item>
+
+                        <!-- 判断题标准答案 -->
+                        <el-form-item label="标准答案:" v-if="answerOption.questionType == 3">
+                            <el-select v-model="answerOption.standardAnswer" placeholder="请选择">
+                                <el-option value="T" label="T"></el-option>
+                                <el-option value="F" label="F"></el-option>
+                            </el-select>
+                        </el-form-item>
+
+                        <!-- 多选题标准答案 -->
+                        <el-form-item label="标准答案:" v-if="answerOption.questionType == 2">
+                            <el-checkbox-group v-model="mulAnswerList" @change="MulAnswerChange">
+                                <el-checkbox 
+                                    v-for="(item, index) in answerOption.optionsSize" 
+                                    :key="index"
+                                    :label="answerList[index]" 
+                                    :value="answerList[index]"
+                                ></el-checkbox>
+                            </el-checkbox-group>
+                        </el-form-item>
+
+                        <el-form-item label="满分:">
+                            <el-input v-model="answerOption.fullScore" maxlength="2" placeholder="请输入"></el-input>
+                        </el-form-item>
+                    </el-form>
+                </div>
+
+                <!-- 多选题给分规则 -->
+                <div class="dialog_group" v-if="answerOption.questionType == 2">
+                    <div class="radio_group_socre">
+                        <el-radio v-model="answerOption.multipleRule" label="1">
+                            <span class="radio_title">给分规则1:</span>全对得满分,少选得
+                            <el-input 
+                                v-model="ruleOneScore" 
+                                class="radio_score" 
+                                maxlength="3"
+                                @input="ScoreChange('ruleOneScore')"
+                            ></el-input>分,选错不得分。
+                        </el-radio>
+                    </div>
+                    <div class="radio_group_socre">
+                        <el-radio v-model="answerOption.multipleRule" label="2">
+                            <span class="radio_title">给分规则2:</span>全对得满分,选对1个
+                            <el-input 
+                                v-model="ruleTwoScore1" 
+                                @input="ScoreChange('ruleTwoScore1')" 
+                                maxlength="3"
+                            ></el-input>分,
+                            选对2个
+                            <el-input 
+                                v-model="ruleTwoScore2" 
+                                @input="ScoreChange('ruleTwoScore2')" 
+                                maxlength="3"
+                            ></el-input>分,
+                            选对3个
+                            <el-input 
+                                v-model="ruleTwoScore3" 
+                                @input="ScoreChange('ruleTwoScore3')" 
+                                maxlength="3"
+                            ></el-input>分,选错不得分
+                        </el-radio>
+                    </div>
+                    <div class="radio_group_long_input">
+                        <el-radio v-model="answerOption.multipleRule" label="3">
+                            <span class="radio_title">给分规则3:</span>
+                            <el-input 
+                                placeholder="A=1,B=2,C=3" 
+                                v-model="ruleThreeStr"
+                                class="input_long_200"
+                            ></el-input>
+                        </el-radio>
+                    </div>
+                </div>
+            </div>
+            <template #footer>
+                <div class="dialog-footer">
+                    <el-button @click="dialogVisible = false">取 消</el-button>
+                    <el-button type="primary" @click="ConfirmEnter()">确 定</el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { ElMessage } from 'element-plus';
+
+const emit = defineEmits(['answerEnter']);
+
+const dialogVisible = ref(false);
+const title = ref('编辑');
+const formRef = ref(null);
+
+const mulAnswerList = ref([]); // 多选答案
+
+const answerList = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']; // 答案列表
+
+// 标准答案编辑对象数据
+const answerOption = reactive({
+    examSubjectId: '', // 考试科目id
+    fullScore: 0, // 满分
+    id: '', // id 修改id
+    multipleRule: '', // 多选规则 类型 1 2 3
+    multipleRuleDesc: null, // 多选题给分分布
+    optionsSize: 4, // 答案数量
+    standardAnswer: "", // 标准答案
+    questionName: "", // 题目名称
+    questionType: "1", // 题目类型 1 单选 2多选 3判断
+});
+
+// 给分规则输入框
+const ruleOneScore = ref(0);
+const ruleTwoScore1 = ref(0);
+const ruleTwoScore2 = ref(0);
+const ruleTwoScore3 = ref(0);
+const ruleThreeStr = ref('');
+
+// 初始化数据
+const InitData = (item) => {
+    Object.assign(answerOption, {
+        examSubjectId: item.examSubjectId,
+        fullScore: item.fullScore, // 满分
+        id: item.id,
+        multipleRule: item.multipleRule,
+        multipleRuleDesc: item.multipleRuleDesc,
+        optionsSize: item.optionsSize,
+        standardAnswer: item.standardAnswer,
+        questionName: item.questionName,
+        questionType: item.questionType,
+    });
+
+    console.log("打印编辑对象", answerOption);
+
+    if (item.multipleRule == '1') {
+        ruleOneScore.value = item.multipleRuleDesc || 0;
+    } else if (item.multipleRule == '2') {
+        const parts = item.multipleRuleDesc?.split(",") || [];
+        ruleTwoScore1.value = parts[0] || 0;
+        ruleTwoScore2.value = parts[1] || 0;
+        ruleTwoScore3.value = parts[2] || 0;
+    } else {
+        ruleThreeStr.value = item.multipleRuleDesc || "";
+    }
+
+    const answerStr = item.standardAnswer?.replace(/\s+/g, '') || '';
+    mulAnswerList.value = answerStr.includes(',') ? answerStr.split(',') : answerStr.split('');
+    
+    dialogVisible.value = true;
+};
+
+// 少选分数验证
+const ScoreChange = (valueKey) => {
+    let val = 0;
+    if (valueKey === 'ruleOneScore') val = ruleOneScore.value;
+    else if (valueKey === 'ruleTwoScore1') val = ruleTwoScore1.value;
+    else if (valueKey === 'ruleTwoScore2') val = ruleTwoScore2.value;
+    else if (valueKey === 'ruleTwoScore3') val = ruleTwoScore3.value;
+
+    if (val > answerOption.fullScore) {
+        if (valueKey === 'ruleOneScore') ruleOneScore.value = 0;
+        else if (valueKey === 'ruleTwoScore1') ruleTwoScore1.value = 0;
+        else if (valueKey === 'ruleTwoScore2') ruleTwoScore2.value = 0;
+        else if (valueKey === 'ruleTwoScore3') ruleTwoScore3.value = 0;
+        
+        ElMessage.warning('分数不能大于满分!');
+    }
+};
+
+// 多选题答案选择事件
+const MulAnswerChange = () => {
+    console.log("打印多选题答案", mulAnswerList.value);
+    answerOption.standardAnswer = mulAnswerList.value.toString(); // 默认的是英文逗号
+    console.log("answerOption", answerOption);
+};
+
+// 题目类型切换事件
+const TypeChange = () => {
+    // 单选题
+    if (answerOption.questionType == 1) {
+        answerOption.multipleRuleDesc = "";
+        answerOption.multipleRule = "";
+        answerOption.standardAnswer = "";
+    } 
+    // 多选
+    else if (answerOption.questionType == 2) {
+        answerOption.multipleRuleDesc = "";
+        answerOption.multipleRule = "1"; // 给分规则默认1
+        answerOption.standardAnswer = "";
+        mulAnswerList.value = [];
+    } 
+    // 判断题
+    else if (answerOption.questionType == 3) {
+        answerOption.multipleRuleDesc = "";
+        answerOption.multipleRule = "";
+        answerOption.standardAnswer = "";
+    }
+    console.log("切换题目类型打印", answerOption);
+};
+
+// 确定弹窗
+const ConfirmEnter = () => {
+    // 给分规则1
+    if (answerOption.multipleRule == '1') {
+        answerOption.isMul = true;
+        answerOption.multipleRuleDesc = ruleOneScore.value || 0;
+    } 
+    // 给分规则2
+    else if (answerOption.multipleRule == '2') {
+        answerOption.isMul = true;
+        answerOption.multipleRuleDesc = `${ruleTwoScore1.value},${ruleTwoScore2.value},${ruleTwoScore3.value}`;
+    }
+    // 给分规则3
+    else if (answerOption.multipleRule == '3') {
+        // 这里需要做下处理 this.ruleThreeStr 里的逗号需要转义成英文的逗号
+        answerOption.multipleRuleDesc = ruleThreeStr.value.replace(/,/g, ',');
+
+        if (validateMultipleQuestionsName(answerOption.multipleRuleDesc)) {
+            answerOption.isMul = true;
+        } else {
+            ElMessage.warning('请填写正确的格式!');
+            return;
+        }
+    }
+
+    emit('answerEnter', { ...answerOption });
+    dialogVisible.value = false;
+};
+
+// 校验格式是否为 字母=内容,字母=内容 的格式
+const validateMultipleQuestionsName = (value) => {
+    console.log("校验格式", value);
+    // 如果为空,直接返回true(或根据业务需求决定)
+    if (!value || value.trim() === '') {
+        return true;
+    }
+    // 分割成各个部分
+    const parts = value.split(',');
+    // 检查每个部分是否符合 字母=内容 的格式
+    for (let part of parts) {
+        // 去掉首尾空格
+        part = part.trim();
+        
+        // 检查是否符合 字母=内容 的格式(支持多个字母)
+        if (!/^[A-Za-z]+=.+$/.test(part)) {
+            return false;
+        }
+    }
+    return true;
+};
+
+// 暴露方法供父组件调用
+defineExpose({
+    InitData
+});
+</script>
+
+<style lang="scss" scoped>
+/* 保持原有样式 */
+</style>

+ 569 - 0
src/views/exam/components/UploadImage.vue

@@ -0,0 +1,569 @@
+<template>
+    <div class="page_dialog">
+        <el-dialog 
+            :title="schoolName + '导入扫描图片'" 
+            :model-value="showDialog" 
+            width="940px" 
+            @close="HandleClose" 
+            v-draggable
+        >
+            <div class="dialog_center">
+                <div class="center_header">
+                    <div class="header_left">
+                         <!-- 隐藏的文件输入框,用于选择文件夹 -->
+                         <input 
+                                type="file" 
+                                ref="fileInput"
+                                style="display: none"
+                                webkitdirectory
+                                directory
+                                multiple
+                                @change="HandleFolderSelect" />
+                        <el-button 
+                            type="primary" 
+                            @click="SelectFolder">
+                            选择文件夹
+                            <i class='el-icon-folder el-icon--right'></i>
+                        </el-button>
+                        <div class="left_item">
+                            <span class="remain_count">(剩余{{ remainCount }}个待上传)</span>
+                        </div>
+                        <div class="left_item" style="display: none;">   
+                            <span style="width: 80px;">灵敏度:</span>
+                            <el-input v-model="sensitive" placeholder="0-255之间得奇数"></el-input>
+                        </div>    
+                        <div class="left_item" style="display: none;">
+                            算法类型:
+                            <el-select style="margin-right: 6px;" v-model="onnx" placeholder="算法类型" >
+                                <el-option label="正常算法" :value="0"></el-option>
+                                <el-option label="废资源算法" :value="1"></el-option>
+                            </el-select>
+                        </div>
+                    </div>
+                    <div class="header_right">
+                        <div class="upload_info" v-if="selectedFiles.length > 0">
+                            <p>已选择文件:{{ selectedFiles.length }}个</p>
+                        </div>
+                    </div>
+                </div>
+                
+                <!-- 图片预览列表区域 -->
+                <div class="center_pic_list" v-if="selectedFiles.length > 0" style="height: 400px !important; max-height: 400px !important;">
+                    <div class="list_img_content">
+                        <div class="list_img_item" v-for="(file, index) in selectedFiles" :key="index" v-show="file && file.uploadStatus !== undefined && file.uploadStatus !== 0">
+                            <div class="item_batch_name">{{ FormatFileName(file.name) }}  {{ file.uploadStatus }}</div>
+                            <div class="item_batch_img">
+                                <img :src="file.url" :alt="file.name">
+                                <!-- 查看按钮,仅在非上传中状态显示 -->
+                                <div class="button_group" v-if="file.uploadStatus != -1">
+                                    <div class="pic_button_view" @click="OpenViewPaper(file.url)">
+                                        <i class="iconfont icon_xianshimima"></i>查看
+                                    </div>
+                                </div>
+                                <!-- 上传进度条 -->
+                                <div class="item_batch_progress" v-if="file.uploadStatus == -1">
+                                    <div>上传中...</div>
+                                    <el-progress :percentage="file.uploadProgress"></el-progress>
+                                </div>
+                                <!-- 上传失败提示 -->
+                                <div class="item_batch_failed" v-if="file.uploadStatus == 1">
+                                    <div style="color:#F56C6C;">上传失败</div>
+                                </div>
+                                <!-- 上传成功提示 -->
+                                <div class="item_batch_success" v-if="file.uploadStatus == 0">
+                                    <div style="color:green;">上传成功</div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div> 
+            </div>
+            
+            <template #footer>
+                <div class="dialog-footer">
+                    <el-button @click="CancelUpload">取消</el-button>
+                    <el-button 
+                        type="primary" 
+                        style="width: 96px;" 
+                        :loading="loading" 
+                        @click="DebounceUpload()"
+                        :disabled="selectedFiles.length === 0">
+                        确定
+                    </el-button>
+                </div>
+            </template>
+        </el-dialog>
+        <!-- 图片预览弹窗组件 -->
+        <!-- <PaperPreview v-model="showPaperPreview" :imageList="previewImageList"></PaperPreview> -->
+    </div>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted } from 'vue';
+
+import axios from 'axios';
+import { ElMessage } from 'element-plus';
+// import PaperPreview from '@/views/examDetailsEdit/components/ImagePreview.vue';
+import { useExamStore } from '@/store/exam'
+import {getCurrentBatchNo,} from '@/api/exam'
+// 实例化 Store
+const examStore = useExamStore()
+
+// 定义组件事件
+const emit = defineEmits(['update:modelValue', 'UploadComplete', 'UploadProgress', 'close', 'CancelUpload']);
+
+// 定义组件属性
+const props = defineProps({
+    modelValue: {
+        type: Boolean,
+        required: true,
+    },
+    paperSchema: {
+        type: [String, Number],
+        default: '1' // 单双面 1单面 2双面
+    },//选择题判分都是单面 
+    isfilter: {
+        type: [String, Number],
+        default: 1 // 是否过滤 1是 0否
+    },
+    // heightRatio: {
+    //     type: [String, Number],
+    //     default: 0.03 // 占打分框高度比例
+    // },
+    // schoolInfo: {
+    //     type: Object,
+    //     default: () => ({
+    //         schoolId: '',
+    //         schoolName: ''
+    //     })
+    // },
+});
+
+// --- 响应式状态定义 ---
+const loading = ref(false); // 加载状态
+const selectedFiles = ref([]); // 已选择的文件列表
+const showPaperPreview = ref(false); // 是否显示预览弹窗
+const previewImageList = ref([]); // 预览图片列表
+const batchNumber = ref(''); // 批次号
+const batchNOID = ref(''); // 批次ID
+const sensitive = ref('165'); // 灵敏度参数
+const onnx = ref(0); // 算法类型 0正常 1废资源
+const isUploading = ref(false); // 是否正在上传中
+const debounceTimer = ref(null); // 防抖定时器
+const fileInput = ref(null); // 文件输入框引用
+
+// 学校相关信息
+const schoolName = ref('');
+// 考试科目 ID
+const schoolId = computed(() => {
+  return examStore.currentExam?.schoolInfoId
+})//计算属性
+
+// --- 计算属性 ---
+
+/**
+ * 控制弹窗显示的双向绑定
+ */
+const showDialog = computed({
+    get: () => props.modelValue,
+    set: (val) => emit('update:modelValue', val)
+});
+
+/**
+ * 获取当前考试科目ID
+ */
+// 考试科目 ID
+const examSubjectId = computed(() => {
+  return examStore.currentExam?.id
+})//计算属性
+
+/**
+ * 计算剩余待上传的文件数量
+ */
+const remainCount = computed(() => {
+    return selectedFiles.value.filter(file => file.uploadStatus !== 0).length;
+});
+
+// --- 监听器 ---
+
+/**
+ * 监听学校信息变化,更新本地学校名称和ID
+ */
+// watch(() => props.schoolInfo, (newVal) => {
+//     if (newVal) {
+//         schoolId.value = newVal.schoolId || '';
+//         schoolName.value = (newVal.schoolName || '') ;
+//     }
+// }, { immediate: true });
+
+// --- 生命周期 ---
+onMounted(() => {
+    console.log("UploadImage组件挂载,当前学校信息:", props.schoolInfo);
+});
+
+// --- 方法定义 ---
+
+/**
+ * 格式化文件名,只显示前5位和后12位
+ * @param {string} fileName - 原始文件名
+ * @returns {string} 格式化后的文件名
+ */
+const FormatFileName = (fileName) => {
+    if (fileName.length <= 17) {
+        return fileName;
+    }
+    const prefix = fileName.substring(0, 5);
+    const suffix = fileName.substring(fileName.length - 12);
+    return prefix + '...' + suffix;
+};
+
+/**
+ * 触发文件选择框点击事件
+ */
+const SelectFolder = () => {
+    const input = fileInput.value;
+    if (input) {
+        input.setAttribute('webkitdirectory', 'true');
+        input.setAttribute('directory', 'true');
+        input.click();
+    }
+};
+
+/**
+ * 处理文件夹选择后的文件流
+ * @param {Event} event - 输入框变更事件
+ */
+const HandleFolderSelect = (event) => {
+    const files = Array.from(event.target.files);
+    ProcessSelectedFiles(files);
+};
+
+/**
+ * 处理并过滤选中的文件,只保留图片并添加到列表
+ * @param {Array} files - 原始文件数组
+ */
+const ProcessSelectedFiles = (files) => {
+
+    //过滤出图片文件
+    const imageFiles = files.filter(file => IsImageFile(file.name));
+    
+    if (imageFiles.length === 0) {
+        ElMessage.warning('未找到图片文件');
+        return;
+    }
+
+    imageFiles.forEach(file => {
+        // 去重检查
+        const exists = selectedFiles.value.some(f => f.name === file.name);
+        if (!exists) {
+            selectedFiles.value.push({
+                name: file.name,
+                url: GetFilePreviewUrl(file),
+                file: file,
+                uploadStatus: -2, // -2: 未上传, -1: 上传中, 0: 成功, 1: 失败
+                uploadProgress: 0,
+            });
+        }
+    });
+    ElMessage.success(`成功选择 ${imageFiles.length} 张图片`);
+};
+
+/**
+ * 判断文件是否为图片格式
+ * @param {string} fileName - 文件名
+ * @returns {boolean} 是否为图片
+ */
+const IsImageFile = (fileName) => {
+    const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
+    const lowerFileName = fileName.toLowerCase();
+    return imageExtensions.some(ext => lowerFileName.endsWith(ext));
+};
+
+/**
+ * 获取文件的本地预览URL
+ * @param {File} file - 文件对象
+ * @returns {string} Blob URL
+ */
+const GetFilePreviewUrl = (file) => {
+    if (window.URL && file) {
+        return window.URL.createObjectURL(file);
+    }
+    return ''; 
+};
+
+/**
+ * 防抖处理的上传触发函数
+ */
+const DebounceUpload = () => {
+    if (debounceTimer.value) {
+        clearTimeout(debounceTimer.value);
+    }
+    debounceTimer.value = setTimeout(() => {
+        EnterUpload();
+    }, 500);
+};
+
+/**
+ * 执行具体的上传逻辑
+ */
+const EnterUpload = async () => {
+    if (isUploading.value) {
+        ElMessage.warning("正在导入图片中,请勿重复点击!");
+        return;
+    }
+    if (selectedFiles.value.length === 0) {
+        ElMessage.warning('请先选择图片');
+        return;
+    }
+
+    // // 校验灵敏度参数
+    // const sensVal = Number(sensitive.value);
+    // if (!sensitive.value || isNaN(sensVal) || sensVal <= 0 || sensVal >= 255 || sensVal % 2 === 0) {
+    //     ElMessage.warning('灵敏度参数只能是0-255之间的奇数,不包含0和255');
+    //     return;
+    // }
+
+    // // 校验双面扫描图片数量
+    // if (props.paperSchema == 2 && selectedFiles.value.length % 2 != 0) {
+    //     ElMessage.warning('图片数量不对!双面扫描的图片数量必须为偶数');
+    //     return;
+    // }
+
+    isUploading.value = true;
+    loading.value = true;
+
+    try {
+        // 1. 获取批次号
+        const param = {
+            examSubjectId: examSubjectId.value,
+            schoolId: schoolId.value,
+        };
+    
+        const res = await  getCurrentBatchNo(param);
+        
+        batchNumber.value = res.data.batchNo;
+        batchNOID.value = res.data.id;
+
+        // 通知父组件上传开始
+        emit('UploadComplete', batchNumber.value, selectedFiles.value.length, batchNOID.value);
+
+        const uploadResults = [];
+
+        // 2. 逐个上传文件
+        for (let i = 0; i < selectedFiles.value.length; i++) {
+            let fileItem = selectedFiles.value[i];
+            if (!fileItem) continue;
+            fileItem.uploadStatus = -1; // 标记为上传中
+            
+            const result = await UploadSingleFile(fileItem.file, i + 1);
+            console.log("打印上传结果",result);
+            uploadResults.push(result);
+
+            // 模拟进度条动画效果
+            let progress = 0;
+            const interval = setInterval(() => {
+                progress += 5;
+                if (progress >= 100) {
+                    progress = 100;
+                    clearInterval(interval);
+                    if (result.success) {
+                        fileItem.uploadProgress = 100;
+                        fileItem.uploadStatus = 0;
+   
+                    } else {
+                        fileItem.uploadProgress = 0;
+                        fileItem.uploadStatus = 1; // 失败状态
+                    }
+                    // fileItem.uploadProgress = progress;
+                    // // 根据上传结果设置最终状态
+                    // fileItem.uploadStatus = result.success ? 0 : 1;
+                } else {
+                    fileItem.uploadProgress = progress;
+                }
+            }, 100);
+            
+            // 通知父组件上传进度
+            emit('UploadProgress', batchNumber.value, uploadResults.length, batchNOID.value);
+        }
+      
+        // 3. 上传完成后的处理
+        setTimeout(() => {
+            HandleClose();
+            ElMessage.success(`成功上传 ${uploadResults.length} 张图片`);
+        }, 3000);
+
+    } catch (error) {
+        console.error('上传过程中发生错误:', error);
+        ElMessage.error('上传过程中发生错误');
+    } finally {
+        loading.value = false;
+        isUploading.value = false;
+    }
+};
+
+/**
+ * 上传单个文件到服务器
+ * @param {File} file - 文件对象
+ * @param {number} index - 文件序号
+ * @returns {Promise} 上传结果
+ */
+const UploadSingleFile = (file, index) => {
+    return new Promise((resolve) => {
+        const formData = new FormData();
+        formData.append('file', file);
+        
+        // 构建 JSON 参数
+        let jsonParam = {
+            examSubjectId: examSubjectId.value,
+            batchNumber: batchNumber.value,
+            sensitive: sensitive.value,
+            filter: 0,
+            heightRatio: 0.03,
+            schoolId: schoolId.value,
+        };
+        if (onnx.value == 1) {
+            jsonParam.onnx = 1;//算法类型 1-表示废资源算法,也就是识别一张要花15S,不传的话是正常算法,放在页面让用户选择。
+        }
+
+        formData.append('jsonParam', JSON.stringify(jsonParam));
+        formData.append('seqNumber', index);
+        formData.append('paperSchema', props.paperSchema);
+
+        // 根据环境确定上传地址
+        let uploadUrl = '/api/v1/ai_exam_scan/upload_img';
+        if (import.meta.env.MODE  === 'production') {
+            uploadUrl = '/teaching/api/v1/ai_exam_scan/upload_img';//线上环境
+        }
+
+        axios.post(uploadUrl, formData, {
+            headers: {
+                'Authorization': localStorage.getItem('token'),
+                'Content-Type': 'multipart/form-data'
+            }
+        }).then(response => {
+            const data = response.data;
+            resolve({
+                fileName: file.name,
+                success: data.code === 200,
+                data: data,
+                file: file
+            });
+        }).catch(error => {
+            console.error(`文件 ${file.name} 上传失败:`, error);
+            resolve({
+                fileName: file.name,
+                success: false,
+                error: error.message,
+                file: file
+            });
+        });
+    });
+};
+
+/**
+ * 打开图片预览弹窗
+ * @param {string} url - 图片URL
+ */
+const OpenViewPaper = (url) => {
+    showPaperPreview.value = true;
+    previewImageList.value = [{ url }];
+};
+
+/**
+ * 关闭弹窗并重置状态
+ */
+const HandleClose = () => {
+    showDialog.value = false;
+    selectedFiles.value = [];
+    if (fileInput.value) {
+        fileInput.value.value = '';
+    }
+    emit('close');
+};
+
+/**
+ * 取消上传并重置状态
+ */
+const CancelUpload = () => {
+    selectedFiles.value = [];
+    if (fileInput.value) {
+        fileInput.value.value = '';
+    }
+    emit('CancelUpload', batchNumber.value);
+    showDialog.value = false;
+};
+</script>
+
+<style lang="scss" scoped>
+/* 保持原有样式布局 */
+.center_header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+}
+.header_left {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+.list_img_content {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+    overflow-y: auto;
+    height: 100%;
+}
+.list_img_item {
+    width: 120px;
+    border: 1px solid #eee;
+    padding: 5px;
+    border-radius: 4px;
+    position: relative;
+}
+.item_batch_name {
+    font-size: 12px;
+    text-align: center;
+    margin-bottom: 5px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+.item_batch_img {
+    position: relative;
+    img {
+        width: 100%;
+        height: 80px;
+        object-fit: cover;
+        border-radius: 2px;
+    }
+}
+.button_group {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 80px; /* 与图片高度一致 */
+    background: rgba(0,0,0,0.5);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    color: white;
+    cursor: pointer;
+    opacity: 0;
+    transition: opacity 0.3s;
+}
+.list_img_item:hover .button_group {
+    opacity: 1;
+}
+.item_batch_progress, .item_batch_failed, .item_batch_success {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    background: rgba(255,255,255,0.9);
+    font-size: 10px;
+    text-align: center;
+    padding: 2px 0;
+}
+</style>

+ 30 - 2
src/views/exam/question.vue

@@ -122,7 +122,7 @@
                 <el-table-column prop="name" label="操作" width="150">
                     <template v-slot="scope">
                         <div class="ele_button table_row_button">
-                            <span class="btn_editor">编辑</span>
+                            <span class="btn_editor" @click="EditorSingle(scope.row)">编辑</span>
                             <span class="btn_delete" @click="DeleteSingle(scope.row)">删除</span>
                         </div>
                    </template>
@@ -137,6 +137,7 @@
         </div>
     </div>
     <ChooseTemplate v-model="showChooseTemplate"></ChooseTemplate>
+    <EditorQuestion ref="enterStandardEdit" @AnswerEnter="EditorAnswerEnter"></EditorQuestion>
   </div>
 </template>
 <script lang="ts" setup>
@@ -144,10 +145,13 @@ import { useExamStore } from '@/store/exam'
 import { useRouter } from 'vue-router'
 import { onMounted,computed ,onUnmounted,ref,nextTick } from 'vue';
 import ChooseTemplate from './components/chooseTemplate.vue';
+import EditorQuestion from './components/EditorQuestion.vue';
+
 import { ElMessageBox,ElMessage } from 'element-plus' 
 import { getSmartQuestionList,getTemplateInfo,setQuestion,saveQuestion,deleteAllQuestion,deleteSingleQuestion} from '@/api/exam'
 
 
+
 // 实例化 Store
 const examStore = useExamStore()
 const router = useRouter()
@@ -178,6 +182,7 @@ const templateName=ref('机读卡');
 const templateQuestionCount=ref(0);//模板题目总数量
 const showChooseTemplate=ref(false);// 弹出选择模板的弹窗
 const tableData = ref<QuestionItem[]>([])
+const enterStandardEdit=ref<HTMLElement>();//标准答案编辑组件实例
 
 const tableHeight = ref(400)// 表格高度
 const answerList=['A','B','C','D','E','F','G','H','I','J','K']//答案列表
@@ -324,7 +329,7 @@ const DeleteAll=()=>{
 }
 
 //确定完成
-const FinishQuestion=()=>{
+const FinishQuestion=async ()=>{
     console.log("打印完成")
     ElMessageBox.confirm('确认完成吗?', '提示', {
         confirmButtonText: '确定',
@@ -346,6 +351,23 @@ const FinishQuestion=()=>{
     })
 }
 
+//确定编辑
+const EditorAnswerEnter=async (item:any)=>{
+    console.log("打印确定编辑",item)
+    let params=[];
+    params.push(item);
+    const res = await saveQuestion(params)
+    if(res.code == 200) 
+    {
+        ElMessage.success("保存成功!")
+        GetSmartQuestionList();
+    }
+    else
+    {
+        ElMessage.error(res.msg)
+    }
+}
+
 //删除单个题目 需要二次弹窗确认提示
 const DeleteSingle=(item:any)=>{
     console.log("打印删除单个",item)
@@ -372,6 +394,12 @@ const DeleteSingle=(item:any)=>{
         }
     })
 }
+
+//编辑单个题目
+const EditorSingle=(item:any)=>{
+    console.log("打印编辑单个题目",item)
+    enterStandardEdit.value.InitData(item);
+}
 // 4. 计算高度的函数
 const CalculateTableHeight = () => {
 

+ 78 - 6
src/views/exam/scanList.vue

@@ -25,7 +25,7 @@
                 <el-button @click="GoSearch()"  slot="append" icon="el-icon-search"></el-button>
             </el-input> -->
         </div>
-        <div class="content_right">
+        <div class="content_right el_button">
             <el-button @click="Refresh" >
                 <i class="iconfont icon_shuaxin"></i>刷新
             </el-button>
@@ -34,7 +34,9 @@
             
             <!-- <el-button class="delete_item" type="text" @click="OpenDeleteAllDialog"  v-if="tableData.length>0">删除所有</el-button> -->
             <el-button @click="OpenReIdentify" >重新识别</el-button>
+            <div @click="OpenImportDialog" class="editor_item" >导入扫描图片</div>
             <el-button @click="OpenEditExamList()" type="primary">编辑考场名单</el-button>
+            
             <!-- <el-button @click="OpenImportStudent()" type="primary" v-else>导入考场名单</el-button> -->
         </div>
     </div>
@@ -160,9 +162,7 @@
             </div>
         </div>
     </div>
-    <ReIdentify v-model="isReIdentify" :reIdentifyTotal="reIdentifyTotal" justObjective="0" :schoolInfo="selectSchoolInfo"  @ReIdentifyComplete="Refresh"></ReIdentify>
-    <!-- <SelectStudent v-model="showSelectStudent"  @success="StudentSuccess"></SelectStudent> -->
-    <AbnormalDetail v-model="showAbnormalDialog" :scanState="scanState" @success="BackScan"></AbnormalDetail>
+    
     <div class="page_dialog">
           <el-dialog
             v-model="showNumberSetDialog"
@@ -192,6 +192,9 @@
             </span>
             </template>
         </el-dialog>
+        <ReIdentify v-model="isReIdentify" :reIdentifyTotal="reIdentifyTotal" justObjective="0" :schoolInfo="selectSchoolInfo"  @ReIdentifyComplete="Refresh"></ReIdentify>
+        <UploadImage v-if="showUploadImage" v-model="showUploadImage"  :batchNumber="currentBatchNo" :isfilter="isfilter"  :schoolInfo="selectSchoolInfo"  @UploadProgress="UploadProgress" @UploadComplete="UploadComplete"  @CancelUpload="CancelUpload"></UploadImage>
+        <AbnormalDetail v-model="showAbnormalDialog" :scanState="scanState" @success="BackScan"></AbnormalDetail>
     </div>
   </div>
 </template>
@@ -200,8 +203,8 @@ import { useExamStore } from '@/store/exam'
 import { useUserStore } from '@/store/user'
 import { useRouter } from 'vue-router'
 import { onMounted ,ref,computed,onUnmounted,nextTick } from 'vue';
-import ScanButton from './components/scanButton.vue'
-import SelectStudent from './components/selectStudent.vue'
+import ScanButton from './components/scanButton.vue'//扫描按钮
+import UploadImage from './components/UploadImage.vue'//导入扫描图片
 import {setExamNumberPosition,finishScanned,getExamScoreSummary,hasImportStudent,getBatchList,getCurrentBatchNo,deleteBatch,updateScanCount,setExamScanSetting,saveChooseCode} from '@/api/exam'
 import scanCommon from '@/utils/scanCommon';
 import { ElMessageBox, ElMessage } from 'element-plus'
@@ -290,6 +293,9 @@ const scanClientVersion=ref('');//客户端版本
 const isScanning=ref(false);//是否正在扫描 扫描状态
 const hasMakeTemplate=ref(false);//模版是否制作完成
 const currentBatchNo=ref('');//当前批次号
+
+const isfilter=ref(1);//是否过滤
+
 const currentBatchNoId=ref('');//当前批次号id
 const loadingText=ref('');//加载文本
 const baseUrl=import.meta.env.VITE_API_BASE_URL;
@@ -338,6 +344,7 @@ const useDriveUI=ref(0);//1是会弹框,其他值或不写则不弹
 
 const isImportStudent=ref(false);//是否导入了学生名单
 const showSelectStudent=ref(false);//是否显示选择学生名单弹窗
+const showUploadImage=ref(false);//导入扫描图片弹窗
 const finishLoading=ref(false);//扫描完成加载中
 const scanDataInfo=ref({
     abnormalNum:0,//异常数量
@@ -369,6 +376,66 @@ const Refresh = () => {
     GetScanBatchList();
 }
 
+//上传进度设置
+const UploadProgress=(batchNumber:number,scanNumber:number)=>{
+
+    console.log("图片上传扫描更新进度",batchNumber,scanNumber);
+    const targetItem = tableData.value.find((item:any) => item.batchNo == batchNumber);
+    
+    if (targetItem) {
+
+        
+    }
+}
+
+//导入图片完成
+const UploadComplete=(batchNumber:number,scanNumber:number,currentBatchNOID:string) => { 
+
+     let count = scanNumber;
+
+    // // 1:单面    2:双面
+    // if (paperSchema.value == 2) {
+    //     // 双面扫描数量除以2(向下取整,防止奇数报错)
+    //     count = Math.floor(scanNumber / 2);
+    // } else {
+        // 单面不除以2
+        count = scanNumber;
+    // }
+
+    currentBatchNo.value = batchNumber.toString();
+
+    const newData = {
+        batchNo: currentBatchNo.value,
+        scanUserName: userInfo.value?.userName || '', // 增加空值保护
+        scannedPaperNum: count, // 扫描张数
+        scannedTime: Date.now(), // 扫描时间
+        uploadNum: 0, // 上传张数
+        uploadStatus: 0,
+    };
+
+    tableData.value.push(newData);
+    
+    loadingText.value = "正在上传中";
+
+    // 建立与后端的websocket连接
+    WebSocketLink(); 
+    
+    // 扫描完成更新扫描张数
+    UpdateBatchScanNumber(currentBatchNOID, count);
+}
+
+//取消上传
+const CancelUpload=(batchNumber:number)=>{
+    const targetItem = tableData.value.find((item:any)=> item.batchNo == batchNumber);
+                
+    setTimeout(() => {
+        if (targetItem) 
+        {
+            targetItem.uploadStatus = 1; 
+        }
+    }, 5000);
+}
+
 //扫描异常返回
 const BackScan= () => { 
     //更新列表
@@ -833,6 +900,11 @@ const OpenImportStudent = () => {
     showSelectStudent.value=true;
 }
 
+//打开上传图片
+const OpenImportDialog=() => {
+    showUploadImage.value=true;
+}
+
 // 打开编辑考试名单
 const OpenEditExamList = () => {
     router.push({