Преглед изворни кода

feat: 新增文件上传题型组件及多语言配置

1.  新增setFile.vue和file.vue组件,实现文件上传、预览、下载功能
2.  新增多语言文件上传相关的国际化文案
3.  在作业页面中注册文件题型组件,适配type84的题型逻辑
4.  修复了photo.vue中删除按钮的文本绑定错误
lsc пре 5 часа
родитељ
комит
b9e09316b2

+ 513 - 0
src/components/pages/workPage/components/file.vue

@@ -0,0 +1,513 @@
+<template>
+  <div class="questionsAndAnswers">
+    <div class="qaa_title">{{ work.answerQ }}</div>
+    <div class="qaa_img" v-if="work.imageList && work.imageList.length > 0">
+      <div v-for="(item, index) in work.imageList" :key="index">
+        <img :src="item.url" alt="" @click="$hevueImgPreview(item.url)">
+      </div>
+    </div>
+    <div class="qaa_type">{{ lang.ssFile }}</div>
+    <!-- 文件列表 -->
+    <div class="imageList" v-if="work.fileList && work.fileList.length > 0">
+      <div v-for="(file, fileIndex) in work.fileList" :key="fileIndex" class="image-item" @click="previewFile(file)">
+        <!-- 图片:缩略图 -->
+        <el-image v-if="isImage(file.url)" style="width: 100px; height: 100px" :src="file.url" fit="cover"></el-image>
+        <!-- 视频:缩略图 + 播放按钮 -->
+        <div v-else-if="isVideo(file.url)" class="video-thumb">
+          <video :src="file.url" muted></video>
+          <div class="play-btn">
+            <svg viewBox="0 0 24 24" width="32" height="32" fill="#fff"><path d="M8 5v14l11-7z"></path></svg>
+          </div>
+        </div>
+        <!-- 其他文件:图标 + 文件名 -->
+        <div v-else class="file-thumb">
+          <svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor">
+            <path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"></path>
+          </svg>
+          <div class="file-name">{{ getFileName(file.url) }}</div>
+        </div>
+        <svg class="delete-icon" @click.stop="delImage(fileIndex)" t="1774237005199" viewBox="0 0 1024 1024" width="20" height="20">
+          <path d="M512 1024A512 512 0 1 1 512 0a512 512 0 0 1 0 1024zM305.956571 370.395429L447.488 512 305.956571 653.604571a45.568 45.568 0 1 0 64.438858 64.438858L512 576.512l141.604571 141.531429a45.568 45.568 0 0 0 64.438858-64.438858L576.512 512l141.531429-141.604571a45.568 45.568 0 1 0-64.438858-64.438858L512 447.488 370.395429 305.956571a45.568 45.568 0 0 0-64.438858 64.438858z" fill="#FF2525" p-id="4716"></path>
+        </svg>
+      </div>
+    </div>
+
+    <!-- 文件预览弹窗 -->
+    <el-dialog
+      :visible.sync="previewVisible"
+      :append-to-body="true"
+      :show-close="true"
+      custom-class="filePreviewDialog"
+      width="80%"
+      @closed="onPreviewClosed"
+    >
+      <div class="previewBody" v-if="currentFile">
+        <!-- 图片预览 -->
+        <img v-if="isImage(currentFile.url)" :src="currentFile.url" class="previewImg" />
+        <!-- 视频预览 -->
+        <video v-else-if="isVideo(currentFile.url)" :src="currentFile.url" controls autoplay class="previewVideo"></video>
+        <!-- Word/PPT/Excel 预览 -->
+        <iframe v-else-if="isOffice(currentFile.url)" :src="officePreviewUrl" class="previewIframe" frameborder="0" allowfullscreen></iframe>
+        <!-- PDF 预览 -->
+        <iframe v-else-if="isPdf(currentFile.url)" :src="pdfPreviewUrl" class="previewIframe" frameborder="0" allowfullscreen></iframe>
+        <!-- 不支持预览:下载 -->
+        <div v-else class="unsupportedBox">
+          <div class="unsupportedTip">{{ lang.ssUnsupportedFile }}</div>
+          <a :href="currentFile.url" download class="downloadBtn">{{ lang.ssDownloadFile }}</a>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- 操作按钮区域 -->
+    <div class="upload-area" v-if="!work.fileList || work.fileList.length == 0">
+      <button class="photo-btn" @click="uploadImage" v-loading="uploadLoading">
+        <span class="plus-icon">+</span>
+        <span>{{ lang.ssUpload }}</span>
+      </button>
+    </div>
+  </div>
+</template>
+
+<script>
+import { uploadOneFile } from '@/components/tools/uploadFile'
+export default {
+  props: {
+    workData: {
+      type: Object,
+      default: () => {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      work: {
+        fileList: []
+      },
+      recordObj: {},
+      isMobile: false,
+      uploadLoading: false,
+      previewVisible: false,
+      currentFile: null
+    };
+  },
+  computed: {
+    officePreviewUrl() {
+      if (!this.currentFile) return "";
+      return `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(this.currentFile.url)}&time=${new Date().getTime()}`;
+      // return `https://view.officeapps.live.com/op/view.aspx?src=${this.currentFile.url)}&time=${new Date().getTime()}`;
+    },
+    pdfPreviewUrl() {
+      if (!this.currentFile) return "";
+      return `https://cloud.cocorobo.cn/pdf.js/web/viewer.html?file=${encodeURIComponent(this.currentFile.url)}&time=${new Date().getTime()}`;
+    }
+  },
+  watch: {
+    work: {
+      deep: true,
+      handler(newValue) {
+        if (JSON.stringify(newValue) != JSON.stringify(this.workData.json)) {
+          this.changeWorkData(newValue);
+        }
+      }
+    },
+    workData: {
+      handler: function (newValue, oldValue) {
+        let _data = JSON.parse(JSON.stringify(this.workData.json));
+        if (JSON.stringify(newValue) != JSON.stringify(oldValue) || JSON.stringify(this.work) != JSON.stringify(_data)) {
+          let _data = JSON.parse(JSON.stringify(this.workData.json));
+          if (!_data.fileList) {
+            _data.fileList = [];
+          }
+          this.work = _data;
+        }
+      },
+      deep: true,
+      immediate: true
+    }
+  },
+  methods: {
+    changeWorkData(newValue) {
+      this.$emit("changeWorkData", JSON.stringify(newValue));
+    },
+    // 获取文件扩展名
+    getFileExt(url) {
+      if (!url) return "";
+      return (url.split("?")[0].split("#")[0].split(".").pop() || "").toLowerCase();
+    },
+    // 获取文件名
+    getFileName(url) {
+      if (!url) return "";
+      return decodeURIComponent(url.split("?")[0].split("#")[0].split("/").pop() || url);
+    },
+    // 判断是否为图片
+    isImage(url) {
+      return ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(this.getFileExt(url));
+    },
+    // 判断是否为视频
+    isVideo(url) {
+      return ["mp4", "webm", "ogg", "mov", "avi", "wmv", "flv", "m4v", "mkv"].includes(this.getFileExt(url));
+    },
+    // 判断是否为 Office 文档(Word/Excel/PPT)
+    isOffice(url) {
+      return ["doc", "docx", "xls", "xlsx", "ppt", "pptx"].includes(this.getFileExt(url));
+    },
+    // 判断是否为 PDF
+    isPdf(url) {
+      return this.getFileExt(url) === "pdf";
+    },
+    // 打开预览弹窗
+    previewFile(file) {
+      this.currentFile = file;
+      this.previewVisible = true;
+    },
+    // 预览弹窗关闭后清理
+    onPreviewClosed() {
+      this.currentFile = null;
+    },
+    takePhoto() {
+      const input = document.createElement('input');
+      input.type = 'file';
+      input.accept = 'image/*';
+      input.capture = 'environment';
+      input.click();
+      input.onchange = (e) => {
+        if (e.target.files[0]) {
+          uploadOneFile(e.target.files[0]).then(res => {
+            res.src = res.url;
+            this.work.fileList = [...this.work.fileList, res];
+            this.changeWorkData(this.work);
+          });
+        }
+      };
+    },
+    uploadImage() {
+      if(this.uploadLoading){
+        return
+      }
+      const input = document.createElement('input');
+      input.type = 'file';
+      input.accept = '*';
+      input.multiple = true;
+      input.click();
+      input.onchange = (e) => {
+        const files = e.target.files;
+        if (files.length > 1){
+          this.$message.error(this.lang.ssOnlyUploadOneFile);
+          return;
+        }
+        if (files && files.length > 0) {
+          // 限制文件大小不超过20MB
+          // const maxSize = 20 * 1024 * 1024; // 20MB
+          // for (let i = 0; i < files.length; i++) {
+          //   if (files[i].size > maxSize) {
+          //     this.$message.error(this.lang.ssFileTooBig);
+          //     return;
+          //   }
+          // }
+          this.uploadLoading = true;
+          const uploadPromises = [];
+          for (let i = 0; i < files.length; i++) {
+            uploadPromises.push(uploadOneFile(files[i]));
+          }
+          Promise.all(uploadPromises).then(results => {
+            const newImages = results.map(res => ({ ...res, src: res.url }));
+            this.work.fileList = [...this.work.fileList, ...newImages];
+            this.uploadLoading = false;
+            this.changeWorkData(this.work);
+          });
+        }
+      };
+    },
+    delImage(index) {
+      this.work.fileList.splice(index, 1);
+      this.changeWorkData(this.work);
+    },
+    checkMobile() {
+      const ua = navigator.userAgent;
+      const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|Android/i.test(ua);
+      const isTablet = /iPad|Android(?!.*Mobile)/i.test(ua);
+      this.isMobile = isMobileDevice || isTablet;
+    }
+  },
+  mounted() {
+    this.checkMobile();
+  },
+  beforeUnmount() {
+  }
+};
+</script>
+
+<style scoped>
+.questionsAndAnswers {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  box-sizing: border-box;
+  padding: 70px 10%;
+  overflow: auto;
+}
+
+.qaa_title {
+  font-size: 2rem;
+  font-weight: bold;
+  text-align: center;
+  width: 100%;
+}
+
+.qaa_img {
+  width: 100%;
+  height: auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.qaa_img>div {
+  max-width: 200px;
+  height: auto;
+  margin: 10px;
+}
+
+.qaa_img>div>img {
+  width: 100%;
+  height: auto;
+  object-fit: cover;
+  cursor: pointer;
+}
+
+.qaa_type {
+  margin: 20px 0 40px 0;
+  font-size: 1rem;
+  color: #808080;
+}
+
+.imageList {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px;
+  margin-bottom: 30px;
+  justify-content: center;
+}
+
+.image-item {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  cursor: pointer;
+}
+
+.video-thumb {
+  position: relative;
+  width: 100px;
+  height: 100px;
+  border-radius: 4px;
+  overflow: hidden;
+  background: #000;
+}
+
+.video-thumb video {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  pointer-events: none;
+}
+
+.play-btn {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  width: 32px;
+  height: 32px;
+  border-radius: 50%;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.file-thumb {
+  width: 100px;
+  height: 100px;
+  border-radius: 4px;
+  background: #f5f7fa;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #3681FC;
+  padding: 8px 4px;
+  box-sizing: border-box;
+}
+
+.file-name {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #666;
+  text-align: center;
+  word-break: break-all;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+
+.delete-icon {
+  position: absolute;
+  top: -10px;
+  right: -10px;
+  cursor: pointer;
+}
+
+.delete-text {
+  margin-top: 8px;
+  color: #FF2525;
+  font-size: .9rem;
+}
+
+.upload-area {
+  display: flex;
+  align-items: center;
+  gap: 40px;
+}
+
+.photo-btn {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 12px 32px;
+  border: 2px solid #FF9500;
+  border-radius: 8px;
+  background-color: #fff;
+  color: #FF9500;
+  font-size: 1rem;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.photo-btn:hover {
+  background-color: #fff8f0;
+}
+
+.photo-btn + .photo-btn {
+  margin-left: 20px;
+}
+
+.plus-icon {
+  font-size: 1.2rem;
+  font-weight: bold;
+}
+
+.upload-btn {
+  color: #67C23A;
+  font-size: 1rem;
+  cursor: pointer;
+  transition: color 0.3s;
+}
+
+.upload-btn:hover {
+  color: #85CE61;
+}
+
+/* 文件预览弹窗 */
+.filePreviewDialog >>> .el-dialog {
+  background: transparent;
+  box-shadow: none;
+}
+
+.filePreviewDialog >>> .el-dialog__header {
+  padding: 0;
+  margin: 0;
+}
+
+.filePreviewDialog >>> .el-dialog__headerbtn {
+  top: -2.5rem;
+  right: -2.5rem;
+  width: 2.5rem;
+  height: 2.5rem;
+  background: rgba(0, 0, 0, 0.5);
+  border-radius: 50%;
+  z-index: 10;
+}
+
+.filePreviewDialog >>> .el-dialog__close {
+  color: #fff;
+  font-size: 1.5rem;
+}
+
+.filePreviewDialog >>> .el-dialog__body {
+  padding: 0;
+  background: transparent;
+}
+
+.filePreviewDialog >>> .el-dialog__headerbtn:hover .el-dialog__close {
+  color: #fff;
+}
+
+.previewBody {
+  width: 100%;
+  background: #000;
+  border-radius: 8px;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 400px;
+}
+
+.previewImg {
+  max-width: 100%;
+  max-height: 80vh;
+  object-fit: contain;
+}
+
+.previewVideo {
+  width: 100%;
+  max-height: 80vh;
+  display: block;
+}
+
+.previewIframe {
+  width: 100%;
+  height: 80vh;
+  border: none;
+}
+
+.unsupportedBox {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 1rem;
+  padding: 3rem 1rem;
+  color: #fff;
+  background: #000;
+}
+
+.unsupportedTip {
+  font-size: 1rem;
+  color: #bbb;
+}
+
+.downloadBtn {
+  display: inline-block;
+  padding: 0.5rem 1.5rem;
+  background: #3681FC;
+  color: #fff;
+  border-radius: 4px;
+  text-decoration: none;
+  font-size: 0.875rem;
+  transition: opacity 0.2s;
+}
+
+.downloadBtn:hover {
+  opacity: 0.85;
+  color: #fff;
+}
+</style>

+ 1 - 1
src/components/pages/workPage/components/photo.vue

@@ -14,7 +14,7 @@
         <svg class="delete-icon" @click="delImage(imageIndex)" t="1774237005199" viewBox="0 0 1024 1024" width="20" height="20">
         <svg class="delete-icon" @click="delImage(imageIndex)" t="1774237005199" viewBox="0 0 1024 1024" width="20" height="20">
           <path d="M512 1024A512 512 0 1 1 512 0a512 512 0 0 1 0 1024zM305.956571 370.395429L447.488 512 305.956571 653.604571a45.568 45.568 0 1 0 64.438858 64.438858L512 576.512l141.604571 141.531429a45.568 45.568 0 0 0 64.438858-64.438858L576.512 512l141.531429-141.604571a45.568 45.568 0 1 0-64.438858-64.438858L512 447.488 370.395429 305.956571a45.568 45.568 0 0 0-64.438858 64.438858z" fill="#FF2525" p-id="4716"></path>
           <path d="M512 1024A512 512 0 1 1 512 0a512 512 0 0 1 0 1024zM305.956571 370.395429L447.488 512 305.956571 653.604571a45.568 45.568 0 1 0 64.438858 64.438858L512 576.512l141.604571 141.531429a45.568 45.568 0 0 0 64.438858-64.438858L576.512 512l141.531429-141.604571a45.568 45.568 0 1 0-64.438858-64.438858L512 447.488 370.395429 305.956571a45.568 45.568 0 0 0-64.438858 64.438858z" fill="#FF2525" p-id="4716"></path>
         </svg>
         </svg>
-        <span class="delete-text">{{ lang.ssDeleteImage }}</span>
+        <!-- <span class="delete-text">{{ lang.ssDelete }}</span> -->
       </div>
       </div>
     </div>
     </div>
 
 

+ 449 - 0
src/components/pages/workPage/components/setFile.vue

@@ -0,0 +1,449 @@
+<template>
+  <div class="setChoiceQuestion">
+    <div class="testItem">
+      <div class="ti_header">
+        <span>{{ lang.ssFileTip }}</span>
+      </div>
+
+      <div class="ti_title">
+        <el-input class="ti_t_input" type="textarea" v-model="jsonData.answerQ" @input="setTestTitle"></el-input>
+        <div class="uploadImage" @click="uploadImage()">
+          <svg viewBox="0 0 1024 1024" version="1.1" p-id="15953" width="200" height="200">
+            <path
+              d="M368 480c-62.4 0-112-49.6-112-112s49.6-112 112-112 112 49.6 112 112-49.6 112-112 112z m0-160c-27.2 0-48 20.8-48 48s20.8 48 48 48 48-20.8 48-48-20.8-48-48-48z m464 608H192c-52.8 0-96-43.2-96-96V192c0-52.8 43.2-96 96-96h640c52.8 0 96 43.2 96 96v640c0 52.8-43.2 96-96 96zM192 160c-17.6 0-32 14.4-32 32v640c0 17.6 14.4 32 32 32h640c17.6 0 32-14.4 32-32V192c0-17.6-14.4-32-32-32H192z m259.2 556.8c-25.6 0-51.2-11.2-70.4-30.4l-38.4-40c-12.8-12.8-33.6-12.8-46.4 0l-49.6 52.8c-12.8 12.8-32 12.8-44.8 1.6s-12.8-32-1.6-44.8l49.6-52.8c17.6-19.2 43.2-30.4 68.8-30.4s51.2 11.2 70.4 30.4l38.4 40c12.8 12.8 33.6 12.8 46.4 0l160-168c17.6-19.2 43.2-30.4 70.4-30.4s51.2 11.2 70.4 30.4L920 628.8c12.8 12.8 11.2 33.6-1.6 44.8-12.8 12.8-33.6 11.2-44.8-1.6L728 518.4c-12.8-12.8-33.6-12.8-46.4 0L521.6 688c-19.2 17.6-44.8 28.8-70.4 28.8z"
+              fill="#333333" p-id="15954"></path>
+          </svg>
+        </div>
+      </div>
+      <!-- 图片 -->
+      <div class="imageList">
+        <div v-for="(image, imageIndex) in jsonData.imageList" :key="imageIndex">
+          <el-image style="width: 100px; height: 100px" :src="image.url" :preview-src-list="[image.url]"
+            fit="cover"></el-image>
+            <svg @click="delImage(imageIndex)" t="1774237005199" viewBox="0 0 1024 1024" width="200" height="200"><path d="M512 1024A512 512 0 1 1 512 0a512 512 0 0 1 0 1024zM305.956571 370.395429L447.488 512 305.956571 653.604571a45.568 45.568 0 1 0 64.438858 64.438858L512 576.512l141.604571 141.531429a45.568 45.568 0 0 0 64.438858-64.438858L576.512 512l141.531429-141.604571a45.568 45.568 0 1 0-64.438858-64.438858L512 447.488 370.395429 305.956571a45.568 45.568 0 0 0-64.438858 64.438858z" fill="#FF2525" p-id="4716"></path></svg>
+        </div>
+      </div>
+
+
+      <!-- 解释说明 -->
+      <div class="explanation" v-show="false">
+        <div class="e_header">
+          <span>{{ lang.ssEvaluationStandard }}</span>
+          <!-- <div v-show="false">
+            <svg t="1774233113566" viewBox="0 0 1027 1024" width="200" height="200">
+              <path
+                d="M511.979521 544.586217a54.525819 54.525819 0 0 1-23.359066-5.247791L31.038758 321.427143a54.397824 54.397824 0 0 1 0-98.236071L488.620455 5.279789c14.719411-7.039718 31.99872-7.039718 46.718131 0l457.581697 217.911283a54.397824 54.397824 0 0 1 0 98.236071l-457.581697 217.911283A54.525819 54.525819 0 0 1 511.979521 544.586217zM180.984761 272.341106L511.979521 429.966801l331.058757-157.625695L511.979521 114.587417 180.984761 272.341106z m354.417823 746.402144l457.517699-217.911283a54.397824 54.397824 0 1 0-46.718131-98.236071L511.979521 909.371625l-434.158634-206.775729a54.397824 54.397824 0 0 0-46.718131 98.236071l457.581697 217.911283a54.525819 54.525819 0 0 0 46.718131 0z m0-239.73441l457.517699-217.847286a54.397824 54.397824 0 1 0-46.718131-98.300068L511.979521 669.573217l-434.158634-206.711731a54.397824 54.397824 0 0 0-46.718131 98.300068l457.581697 217.847286a54.525819 54.525819 0 0 0 46.718131 0z"
+                p-id="9073"></path>
+            </svg>
+            <span>AI生成</span>
+          </div> -->
+        </div>
+        <el-input type="textarea" rows="3" resize="none" v-model="jsonData.evaluationCriteria" :placeholder="lang.ssEnterEvaluationStandard"
+          @input="setTestJson()"></el-input>
+      </div>
+    </div>
+
+  </div>
+</template>
+
+<script>
+import { uploadOneFile } from '@/components/tools/uploadFile'
+export default {
+  emits: ['setTestJson', 'save'],
+  props: {
+    workData: {
+      type: Object,
+      default: () => { }
+    }
+  },
+  data() {
+    return {
+      jsonData: {}
+    }
+  },
+  watch: {
+    workData: {
+      handler(newVal, oldVal) {
+        if (newVal.type == 84) {
+          if (JSON.stringify(newVal.json) != JSON.stringify(this.jsonData)) {
+            let _data = JSON.parse(JSON.stringify(newVal.json))
+
+            if(!_data.imageList){
+              _data.imageList = [];
+            }
+            if(!_data.evaluationCriteria){
+              _data.evaluationCriteria = '';
+            }
+
+            this.jsonData = _data;
+          }
+        }
+      },
+      deep: true,
+      immediate: true
+    }
+  },
+  methods: {
+    // 更新题目
+    setTestJson() {
+      // console.log(this.jsonData)
+      // this.jsonData.testCount = this.jsonData.testJson.length;
+      this.$emit('setTestJson', this.jsonData);
+    },
+    //修改标题
+    setTestTitle( title) {
+      console.log('修改标题', title)
+      this.jsonData.answerQ = title;
+      // let test = this.jsonData..find(item => item.id == id);
+      // if (test) {
+      //   test.teststitle = title;
+      // }
+      this.setTestJson();
+    },
+    // 上传图片
+    uploadImage(id) {
+      const input = document.createElement('input');
+      input.type = 'file';
+      input.accept = 'image/*';
+      input.click();
+      input.onchange = (e) => {
+        if (e.target.files[0]) {
+          uploadOneFile(e.target.files[0]).then(res => {
+            res.src = res.url
+            this.jsonData.imageList = [res];
+            this.$forceUpdate();
+            // this.jsonData.testJson.find(item => item.id == id).timuList = [res];
+            this.setTestJson();
+          })
+        }
+      }
+    },
+    // 删除图片
+    delImage(index) {
+      this.jsonData.imageList.splice(index, 1);
+      // let test = this.jsonData.testJson.find(item => item.id == id);
+      // if (test) {
+      //   test.timuList.splice(index, 1);
+      // }
+      this.setTestJson();
+    },
+  },
+}
+</script>
+
+<style scoped>
+.setChoiceQuestion {
+  width: 100%;
+  height: auto;
+  background: #fff;
+  display: flex;
+  flex-direction: column;
+  overflow: auto;
+  padding: 4rem;
+  box-sizing: border-box;
+  gap: 2rem;
+}
+
+.addTest {
+  border-radius: 5px;
+  border: 1px dashed #d1d5db;
+  padding: .5rem 1rem;
+  height: fit-content;
+  width: fit-content;
+  display: flex;
+  align-items: center;
+  gap: .5rem;
+  cursor: pointer;
+}
+
+.addTest>svg {
+  width: .7rem;
+  height: .7rem;
+  fill: #6E7583;
+}
+
+.addTest>span {
+  font-weight: bold;
+  font-size: .9rem;
+  color: #6b7280;
+}
+
+.testItem {
+  width: 100%;
+  height: auto;
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+  flex-direction: column;
+}
+
+.ti_header {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+}
+
+.ti_header>span {
+  font-size: 1rem;
+  font-weight: 600;
+  color: #ff9300;
+}
+
+.ti_h_edit {
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+.ti_h_edit>span {
+  width: 1.5rem;
+  height: 1.5rem;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #F9FAFB;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.ti_h_edit>span>svg {
+  width: 1rem;
+  height: 1rem;
+  fill: #6B7280;
+}
+
+.ti_title {
+  width: 100%;
+  height: fit-content;
+  padding-bottom: 2rem;
+  min-height: 4rem;
+  background: #FAFBFC;
+  border-radius: 8px;
+  border: 1px solid #E5E7EB;
+  position: relative;
+}
+
+.ti_t_input {
+  width: 100%;
+  height: 100%;
+  padding: 1rem;
+  box-sizing: border-box;
+  border: none;
+  outline: none;
+  background: none;
+  font-size: 1.2rem;
+  resize: none;
+  /* max-height: 10rem; */
+  overflow: auto;
+  /* padding-right: 2rem; */
+}
+
+.ti_t_input /deep/ .el-textarea__inner {
+  padding: 0;
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+  border: none;
+  outline: none;
+  background: none;
+  font-size: 1.2rem;
+  resize: none;
+  /* max-height: 10rem; */
+  overflow: auto;
+}
+
+.uploadImage {
+  position: absolute;
+  bottom: 0.5rem;
+  right: 0.5rem;
+  background: #fff;
+  border-radius: .5rem;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border: 1px solid #E5E7EB;
+  cursor: pointer;
+  padding: .5rem;
+}
+
+.uploadImage>svg {
+  width: 1rem;
+  height: 1rem;
+}
+
+.checkList {
+  width: 100%;
+  height: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+  padding-left: 4rem;
+}
+
+.checkItem {
+  width: 100%;
+  height: auto;
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  gap: .5rem;
+
+}
+
+.check {
+  width: 1.1rem;
+  height: 1.1rem;
+  border: solid 1px #D1D5DB;
+  border-radius: .25rem;
+  cursor: pointer;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.checked {
+  background: #FF9300;
+}
+
+.checked>svg {
+  width: .7rem;
+  height: .7rem;
+  fill: #fff;
+}
+
+.checkItem>.el-input {
+  width: calc(100% - 10rem);
+}
+
+.addCheck {
+  border-radius: 5px;
+  border: 1px dashed #d1d5db;
+  padding: .3rem .8rem;
+  height: fit-content;
+  width: fit-content;
+  display: flex;
+  align-items: center;
+  gap: .5rem;
+  cursor: pointer;
+  margin-left: 2.2rem;
+}
+
+.addCheck>svg {
+  width: .5rem;
+  height: .5rem;
+  fill: #6E7583;
+}
+
+.addCheck>span {
+  font-weight: bold;
+  font-size: .8rem;
+  color: #6b7280;
+}
+
+.btn {
+  display: flex;
+  align-items: center;
+  gap: .25em;
+}
+
+.btn>div {
+  width: 1.5rem;
+  height: 1.5rem;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: #F9FAFB;
+  border-radius: .4px;
+  cursor: pointer;
+}
+
+.btn>div>svg {
+  width: 1rem;
+  height: 1rem;
+  fill: #6B7280;
+}
+
+.explanation {
+  width: 100%;
+  height: auto;
+  padding: 1rem;
+  background: #FAFBFC;
+  border-radius: 5px;
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+  border-radius: .8rem;
+}
+
+.e_header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.e_header>span {
+  color: #6b7280;
+  font-size: .8rem;
+  font-weight: 500;
+}
+
+.e_header>div {
+  display: flex;
+  align-items: center;
+  gap: .5rem;
+  background: #ff9300;
+  color: #fff;
+  padding: .3rem .8rem;
+  font-size: .8rem;
+  font-weight: 500;
+  border-radius: .5rem;
+  cursor: pointer;
+}
+
+.e_header>div>svg {
+  width: .8rem;
+  height: .8rem;
+  fill: #fff;
+}
+
+.imageList {
+  width: 100%;
+  height: auto;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 1rem;
+}
+.imageList>div{
+  width: 100px;
+  height: 100px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  /* overflow: hidden; */
+  border-radius: .5rem;
+  position: relative;
+
+}
+
+.imageList>div:hover>svg{
+  display: flex;
+}
+
+.imageList>div>svg{
+  width: 1rem;
+  height: 1rem;
+  position: absolute;
+  top: -.3rem;
+  right: -.3rem;
+  font-weight: 500;
+  border-radius: .5rem;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  display: none;
+}
+
+</style>

+ 24 - 2
src/components/pages/workPage/index_new.vue

@@ -10,6 +10,8 @@
       @changeWorkData="changeWorkData" />
       @changeWorkData="changeWorkData" />
     <kwlPage ref="kwlPageRef" v-if="workData.type === '831'" :workData="workData"
     <kwlPage ref="kwlPageRef" v-if="workData.type === '831'" :workData="workData"
       @changeWorkData="changeWorkData" />
       @changeWorkData="changeWorkData" />
+    <file ref="fileRef" v-if="workData.type === '84'" :workData="workData"
+      @changeWorkData="changeWorkData" />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -21,6 +23,7 @@ import choiceQuestion from "./components/choiceQuestion.vue";
 import voteQuestion from "./components/voteQuestion.vue";
 import voteQuestion from "./components/voteQuestion.vue";
 import photo from "./components/photo.vue";
 import photo from "./components/photo.vue";
 import kwlPage from "./components/kwlPage.vue";
 import kwlPage from "./components/kwlPage.vue";
+import file from "./components/file.vue";
 export default {
 export default {
   components: {
   components: {
     questionsAndAnswers,
     questionsAndAnswers,
@@ -28,6 +31,7 @@ export default {
     voteQuestion,
     voteQuestion,
     photo,
     photo,
     kwlPage,
     kwlPage,
+    file,
   },
   },
   data() {
   data() {
     return {
     return {
@@ -132,6 +136,8 @@ export default {
               });
               });
             }else if(_data.type == 79){
             }else if(_data.type == 79){
               _data.json.fileList = [];
               _data.json.fileList = [];
+            }else if(_data.type == 84){
+              _data.json.fileList = [];
             }
             }
 
 
             this.workData = _data;
             this.workData = _data;
@@ -295,9 +301,22 @@ export default {
             content: encodeURIComponent(JSON.stringify(this.workData.json)),
             content: encodeURIComponent(JSON.stringify(this.workData.json)),
             type: "831"
             type: "831"
           });
           });
+        } else if (this.workData.type == "84") {
+          //文件题
+          params.push({
+            uid: this.userid,
+            cid: this.courseid,
+            stage: 0,
+            task: task,
+            tool: 0,
+            atool: "84",
+            content: encodeURIComponent(JSON.stringify(this.workData.json)),
+            type: "84"
+          });
         }
         }
+
         if (
         if (
-          ["3", "8", "22", "23", "831"].includes(params[0].type) && 
+          ["3", "8", "22", "23", "831", "84"].includes(params[0].type) && 
           params[0].uid &&
           params[0].uid &&
           params[0].cid
           params[0].cid
         ) {
         ) {
@@ -364,7 +383,7 @@ export default {
             this.loading = false;
             this.loading = false;
             if (_data.length) {
             if (_data.length) {
               _data = _data[0];
               _data = _data[0];
-              if (["15", "45", "78", "79", "831"].includes(_data.atool)) {
+              if (["15", "45", "78", "79", "831", "84"].includes(_data.atool)) {
                 let _work = JSON.parse(decodeURIComponent(_data.content));
                 let _work = JSON.parse(decodeURIComponent(_data.content));
                 this.studentWork = _work;
                 this.studentWork = _work;
                 this.studentWorkToWorkData();
                 this.studentWorkToWorkData();
@@ -417,6 +436,9 @@ export default {
           this.workData.json.fileList = _work.fileList;
           this.workData.json.fileList = _work.fileList;
         }else if(this.workData.type == "831"){
         }else if(this.workData.type == "831"){
           this.workData.json = _work;
           this.workData.json = _work;
+        }else if(this.workData.type == "84"){
+          //文件题
+          this.workData.json.fileList = _work.fileList;
         }
         }
         this.$forceUpdate();
         this.$forceUpdate();
       }
       }

+ 7 - 0
src/components/pages/workPage/setIndex.vue

@@ -12,6 +12,7 @@
     <setVoteQuestion :workData="workData" v-if="workData.type == 78" @setTestJson="setTestJson" @save="saveTest"/>
     <setVoteQuestion :workData="workData" v-if="workData.type == 78" @setTestJson="setTestJson" @save="saveTest"/>
     <setPhoto :workData="workData" v-if="workData.type == 79" @setTestJson="setTestJson" @save="saveTest"/>
     <setPhoto :workData="workData" v-if="workData.type == 79" @setTestJson="setTestJson" @save="saveTest"/>
     <kwlPage :workData="workData" v-if="workData.type == 831" @setTestJson="setTestJson" @save="saveTest"/>
     <kwlPage :workData="workData" v-if="workData.type == 831" @setTestJson="setTestJson" @save="saveTest"/>
+    <setFile :workData="workData" v-if="workData.type == 84" @setTestJson="setTestJson" @save="saveTest"/>
   </div>
   </div>
 </template>
 </template>
 
 
@@ -21,6 +22,7 @@ import setQuestionsAndAnswers from './components/setQuestionsAndAnswers.vue'
 import setVoteQuestion from './components/setVoteQuestion.vue'
 import setVoteQuestion from './components/setVoteQuestion.vue'
 import setPhoto from './components/setPhoto.vue'
 import setPhoto from './components/setPhoto.vue'
 import kwlPage from './components/kwlPage.vue'
 import kwlPage from './components/kwlPage.vue'
+import setFile from './components/setFile.vue'
 
 
 function debounce(func, wait) {
 function debounce(func, wait) {
   let timeout;
   let timeout;
@@ -39,6 +41,7 @@ export default {
     setVoteQuestion,
     setVoteQuestion,
     setPhoto,
     setPhoto,
     kwlPage,
     kwlPage,
+    setFile,
   },
   },
   data() {
   data() {
     return {
     return {
@@ -94,6 +97,10 @@ export default {
             }else if (_data.type == 79) {
             }else if (_data.type == 79) {
               _data.json.answer = "";
               _data.json.answer = "";
               _data.json.fileList = [];
               _data.json.fileList = [];
+            }else if (_data.type == 84) {
+              //文件题
+              _data.json.answer = "";
+              _data.json.fileList = [];
             }
             }
             this.type = _data.type;
             this.type = _data.type;
 
 

+ 8 - 1
src/lang/cn.json

@@ -2091,5 +2091,12 @@
   "ssKWLNoMicPerm":"未获得麦克风权限",
   "ssKWLNoMicPerm":"未获得麦克风权限",
   "ssKWLRecServiceNotReady":"录音服务未就绪",
   "ssKWLRecServiceNotReady":"录音服务未就绪",
   "ssKWLRecStartFail":"启动录音失败",
   "ssKWLRecStartFail":"启动录音失败",
-  "ssKWLRecStopped":"录音已停止"
+  "ssKWLRecStopped":"录音已停止",
+  "ssFileTip": "文件上传指引",
+  "ssFile": "文件上传",
+  "ssUpload": "上传",
+  "ssDownloadFile": "下载文件",
+  "ssUnsupportedFile": "不支持预览此文件类型,请点击下载",
+  "ssFilePreviewTitle": "文件预览",
+  "ssOnlyUploadOneFile": "仅支持上传一个文件"
 }
 }

+ 8 - 1
src/lang/en.json

@@ -2090,5 +2090,12 @@
   "ssKWLNoMicPerm":"Microphone permission not granted",
   "ssKWLNoMicPerm":"Microphone permission not granted",
   "ssKWLRecServiceNotReady":"Recording service is not ready",
   "ssKWLRecServiceNotReady":"Recording service is not ready",
   "ssKWLRecStartFail":"Failed to start recording",
   "ssKWLRecStartFail":"Failed to start recording",
-  "ssKWLRecStopped":"Recording stopped"
+  "ssKWLRecStopped":"Recording stopped",
+  "ssFileTip": "File upload guidance",
+  "ssFile": "File upload",
+  "ssUpload": "Upload",
+  "ssDownloadFile": "Download File",
+  "ssUnsupportedFile": "This file type cannot be previewed, please click to download",
+  "ssFilePreviewTitle": "File Preview",
+  "ssOnlyUploadOneFile": "Only one file can be uploaded"
 }
 }

+ 8 - 1
src/lang/hk.json

@@ -2091,6 +2091,13 @@
   "ssKWLNoMicPerm":"未獲得麥克風權限",
   "ssKWLNoMicPerm":"未獲得麥克風權限",
   "ssKWLRecServiceNotReady":"錄音服務未就緒",
   "ssKWLRecServiceNotReady":"錄音服務未就緒",
   "ssKWLRecStartFail":"啟動錄音失敗",
   "ssKWLRecStartFail":"啟動錄音失敗",
-  "ssKWLRecStopped":"錄音已停止"
+  "ssKWLRecStopped":"錄音已停止",
+  "ssFileTip": "文件上傳指引",
+  "ssFile": "文件上傳",
+  "ssUpload": "上傳",
+  "ssDownloadFile": "下載文件",
+  "ssUnsupportedFile": "不支持預覽此文件類型,請點擊下載",
+  "ssFilePreviewTitle": "文件預覽",
+  "ssOnlyUploadOneFile": "僅支持上傳一個文件"
 }
 }