Sfoglia il codice sorgente

feat: 添加题目图片上传功能,优化选项图片处理

xieyong 1 settimana fa
parent
commit
5b5546b379

+ 10 - 4
src/config/setting.js

@@ -21,7 +21,12 @@ export const SYSTEM_NAME = 'ehs';
 export const REPEATABLE_TABS = [];
 
 // 不需要登录的路由
-export const WHITE_LIST = ['/login', '/forget','/fromQRCode/designDrawing.html','/importTemplate/销售订单导入模板.xlsx'];
+export const WHITE_LIST = [
+  '/login',
+  '/forget',
+  '/fromQRCode/designDrawing.html',
+  '/importTemplate/销售订单导入模板.xlsx'
+];
 
 // 开启 KeepAlive 后仍然不需要缓存的路由地址
 export const KEEP_ALIVE_EXCLUDES = [
@@ -50,9 +55,7 @@ export const REDIRECT_PATH = '/redirect';
 //export const TAB_KEEP_ALIVE = process.env.NODE_ENV !== 'development';
 export const TAB_KEEP_ALIVE = true;
 // 开启 KeepAlive 后需要缓存的路由地址
-export const KEEP_ALIVE_INCLUDE= [
-  'Home',
-];
+export const KEEP_ALIVE_INCLUDE = ['Home'];
 // token 传递的 header 名称
 export const TOKEN_HEADER_NAME = 'Authorization';
 
@@ -78,3 +81,6 @@ export const MAP_KEY = '006d995d433058322319fa797f2876f5';
 // EleAdmin 授权码, 自带的只能用于演示, 正式项目请更换为自己的授权码
 export const LICENSE_CODE =
   'dk9mcwJyetRWQlxWRiojIqJWdzJCLi4Wa4MDNxojI0NWZlJiOiQWaiwCMsICOyRjTEZGWi42bpNnclZnI0nIxEjLxIiO0NW=';
+
+// MAX_IMAGE_SIZE 上传图片大小
+export const MAX_IMAGE_SIZE = 5;

+ 5 - 4
src/views/safetyExam/paperManagement/edit.vue

@@ -244,11 +244,12 @@
     type: '1',
     category: '',
     score: '',
+    questionImage: '',
     options: [
-      { key: 'A', content: '' },
-      { key: 'B', content: '' },
-      { key: 'C', content: '' },
-      { key: 'D', content: '' }
+      { key: 'A', content: '', optionImage: '' },
+      { key: 'B', content: '', optionImage: '' },
+      { key: 'C', content: '', optionImage: '' },
+      { key: 'D', content: '', optionImage: '' }
     ],
     rightAnswer: '',
     rightAnswers: [],

+ 188 - 25
src/views/safetyExam/paperManagement/question-form.vue

@@ -34,15 +34,38 @@
           :value="item.value"
         />
       </el-select>
-      <el-button
-        type="primary"
-        size="small"
-        plain
-        style="margin-left: 10px"
-        @click="handleUploadImage"
-        >上传图片</el-button
+      <el-upload
+        action="#"
+        class="inline-upload"
+        :show-file-list="false"
+        :disabled="isView"
+        :accept="imageAccept"
+        :before-upload="beforeImageUpload"
+        :http-request="handleUploadImage"
       >
+        <el-button
+          type="primary"
+          size="small"
+          plain
+          :loading="questionImageLoading"
+          >上传图片</el-button
+        >
+      </el-upload>
       <span class="upload-tip">试题图片上传</span>
+
+      <div v-if="innerValue.questionImage" class="image-preview">
+        <el-image
+          :src="resolveImageUrl(innerValue.questionImage)"
+          :preview-src-list="[resolveImageUrl(innerValue.questionImage)]"
+          fit="cover"
+          class="image-thumb"
+        />
+        <i
+          v-if="!isView"
+          class="el-icon-error image-remove"
+          @click="handleRemoveImage"
+        />
+      </div>
     </el-form-item>
 
     <el-form-item label="专业类别:" prop="category">
@@ -98,12 +121,37 @@
             placeholder="请输入选项(100字以内)"
             class="option-input"
           />
-          <el-button
-            type="text"
-            size="small"
-            @click="handleUploadOptionImage(index)"
-            >上传图片</el-button
+          <el-upload
+            action="#"
+            class="inline-upload"
+            :show-file-list="false"
+            :disabled="isView"
+            :accept="imageAccept"
+            :before-upload="beforeImageUpload"
+            :http-request="
+              (options) => handleUploadOptionImage(options, index)
+            "
           >
+            <el-button
+              type="text"
+              size="small"
+              :loading="optionImageLoading === index"
+              >上传图片</el-button
+            >
+          </el-upload>
+          <span v-if="item.optionImage" class="image-preview option-preview">
+            <el-image
+              :src="resolveImageUrl(item.optionImage)"
+              :preview-src-list="[resolveImageUrl(item.optionImage)]"
+              fit="cover"
+              class="image-thumb small"
+            />
+            <i
+              v-if="!isView"
+              class="el-icon-error image-remove"
+              @click="handleRemoveOptionImage(index)"
+            />
+          </span>
           <el-button
             v-if="innerValue.type !== '3' && innerValue.options.length > 2"
             type="text"
@@ -135,6 +183,10 @@
 </template>
 
 <script>
+  import { uploadFile } from '@/api/system/file/index';
+  import { getImageUrl } from '@/utils/file';
+  import { MAX_IMAGE_SIZE } from "@/config/setting.js";
+
   const typeOptions = [
     { label: '单选题', value: '1' },
     { label: '多选题', value: '2' },
@@ -164,6 +216,9 @@
       return {
         typeOptions,
         categoryOptions,
+        imageAccept: 'image/png,image/jpeg,image/jpg,image/gif,image/bmp',
+        questionImageLoading: false,
+        optionImageLoading: -1,
         rules: {
           content: [
             { required: true, message: '请输入试题内容', trigger: 'blur' }
@@ -216,17 +271,17 @@
       handleTypeChange(val) {
         if (val === '1' || val === '2') {
           this.innerValue.options = [
-            { key: 'A', content: '' },
-            { key: 'B', content: '' },
-            { key: 'C', content: '' },
-            { key: 'D', content: '' }
+            { key: 'A', content: '', optionImage: '' },
+            { key: 'B', content: '', optionImage: '' },
+            { key: 'C', content: '', optionImage: '' },
+            { key: 'D', content: '', optionImage: '' }
           ];
           this.innerValue.rightAnswer = '';
           this.innerValue.rightAnswers = [];
         } else if (val === '3') {
           this.innerValue.options = [
-            { key: 'A', content: '正确' },
-            { key: 'B', content: '错误' }
+            { key: 'A', content: '正确', optionImage: '' },
+            { key: 'B', content: '错误', optionImage: '' }
           ];
           this.innerValue.rightAnswer = '';
           this.innerValue.rightAnswers = [];
@@ -236,13 +291,75 @@
           this.innerValue.rightAnswers = [];
         }
       },
-      handleUploadImage() {
-        // TODO: 试题图片上传
-        this.$message.info('试题图片上传待接入');
+      async handleUploadImage(options) {
+        this.questionImageLoading = true;
+        try {
+          const storePath = await this.doUpload(options.file);
+          this.$set(this.innerValue, 'questionImage', storePath);
+          this.$message.success('上传成功');
+        } catch (error) {
+          this.$message.error(error.message || '上传失败');
+        } finally {
+          this.questionImageLoading = false;
+        }
+      },
+      handleRemoveImage() {
+        this.$set(this.innerValue, 'questionImage', '');
+      },
+      async handleUploadOptionImage(options, index) {
+        const option = this.innerValue.options[index];
+        if (!option) {
+          return;
+        }
+        this.optionImageLoading = index;
+        try {
+          const storePath = await this.doUpload(options.file);
+          this.$set(option, 'optionImage', storePath);
+          this.$message.success('上传成功');
+        } catch (error) {
+          this.$message.error(error.message || '上传失败');
+        } finally {
+          this.optionImageLoading = -1;
+        }
+      },
+      handleRemoveOptionImage(index) {
+        const option = this.innerValue.options[index];
+        if (option) {
+          this.$set(option, 'optionImage', '');
+        }
       },
-      handleUploadOptionImage(index) {
-        // TODO: 选项图片上传
-        this.$message.info('选项图片上传待接入:' + index);
+      /* 校验图片类型与大小,el-upload 的 before-upload 钩子 */
+      beforeImageUpload(file) {
+        if (!file.type.startsWith('image')) {
+          this.$message.error('只能选择图片');
+          return false;
+        }
+        if (file.size / 1024 / 1024 > MAX_IMAGE_SIZE) {
+          this.$message.error(`图片大小不能超过 ${MAX_IMAGE_SIZE}MB`);
+          return false;
+        }
+        return true;
+      },
+      /* 调用文件上传接口,返回 storePath */
+      async doUpload(file) {
+        const res = await uploadFile({ module: 'main', multiPartFile: file });
+        const storePath = res && res.data && res.data.storePath;
+        if (!storePath) {
+          throw new Error('上传失败');
+        }
+        if(import.meta.env.MODE === 'development') {
+          const url = res && res.data && res.data.url;
+          return url;
+        }
+        return storePath;
+      },
+      /* storePath 拼接为可访问的图片地址 */
+      resolveImageUrl(storePath) {
+        if(storePath.startsWith('http')) {
+          return storePath;
+        }
+        const imageUrl = storePath ? getImageUrl(storePath) : '';
+        return imageUrl;
       },
       handleAddOption() {
         if (this.innerValue.type === '3') {
@@ -252,7 +369,11 @@
         const nextKey = String.fromCharCode(
           65 + this.innerValue.options.length
         );
-        this.innerValue.options.push({ key: nextKey, content: '' });
+        this.innerValue.options.push({
+          key: nextKey,
+          content: '',
+          optionImage: ''
+        });
       },
       handleDeleteOption(index) {
         if (this.innerValue.type === '3') {
@@ -294,6 +415,47 @@
     margin-left: 10px;
   }
 
+  .inline-upload {
+    display: inline-block;
+    margin-left: 10px;
+  }
+
+  .image-preview {
+    position: relative;
+    display: inline-block;
+    margin-left: 8px;
+    line-height: 1;
+
+    .image-thumb {
+      width: 80px;
+      height: 80px;
+      border: 1px solid #dcdfe6;
+      border-radius: 4px;
+
+      &.small {
+        width: 40px;
+        height: 40px;
+      }
+    }
+
+    .image-remove {
+      position: absolute;
+      top: -6px;
+      right: -6px;
+      color: #f56c6c;
+      background: #fff;
+      border-radius: 50%;
+      cursor: pointer;
+      font-size: 16px;
+    }
+  }
+
+  .option-preview {
+    margin-top: 0;
+    margin-right: 10px;
+    vertical-align: middle;
+  }
+
   .option-list {
     .option-item {
       display: flex;
@@ -320,5 +482,6 @@
 
   .danger-text {
     color: #f56c6c;
+    margin-left: 10px;
   }
 </style>

+ 10 - 5
src/views/safetyExam/questionBank/maintenance.vue

@@ -142,11 +142,12 @@
     type: '1',
     category: '',
     score: '',
+    questionImage: '',
     options: [
-      { key: 'A', content: '' },
-      { key: 'B', content: '' },
-      { key: 'C', content: '' },
-      { key: 'D', content: '' }
+      { key: 'A', content: '', optionImage: '' },
+      { key: 'B', content: '', optionImage: '' },
+      { key: 'C', content: '', optionImage: '' },
+      { key: 'D', content: '', optionImage: '' }
     ],
     rightAnswer: '',
     rightAnswers: [],
@@ -285,7 +286,8 @@
           .sort((a, b) => Number(a.sortNo) - Number(b.sortNo))
           .map((option, index) => ({
             key: String.fromCharCode(65 + index),
-            content: option.optionContent || ''
+            content: option.optionContent || '',
+            optionImage: option.optionImage || ''
           }));
         const correctAnswer = question.correctAnswer || '';
         return {
@@ -301,6 +303,7 @@
             question.questionScore === undefined
               ? ''
               : String(question.questionScore),
+          questionImage: question.questionImage || '',
           options,
           rightAnswer: type === '2' ? '' : correctAnswer,
           rightAnswers:
@@ -365,8 +368,10 @@
               ? this.form.rightAnswers.join(',')
               : this.form.rightAnswer,
             questionExplanation: this.form.analysis,
+            questionImage: this.form.questionImage || '',
             optionList: this.form.options.map((item, index) => ({
               optionContent: item.content,
+              optionImage: item.optionImage || '',
               sortNo: index + 1
             }))
           }