Просмотр исходного кода

Merge branch 'master' of http://60.205.254.193:3000/GCLS/eep_page

dsy 2 месяцев назад
Родитель
Сommit
b10afb2e62
27 измененных файлов с 784 добавлено и 49 удалено
  1. 50 0
      src/api/book.js
  2. 269 2
      src/components/CommonPreview.vue
  3. 6 2
      src/components/ExplanatoryNoteDialog.vue
  4. 322 1
      src/views/book/courseware/preview/CoursewarePreview.vue
  5. 2 1
      src/views/book/courseware/preview/components/article/NormalModelChs.vue
  6. 4 2
      src/views/book/courseware/preview/components/article/PhraseModelChs.vue
  7. 2 1
      src/views/book/courseware/preview/components/article/Practicechs.vue
  8. 5 1
      src/views/book/courseware/preview/components/article/Voicefullscreen.vue
  9. 1 0
      src/views/book/courseware/preview/components/article/WordModelChs.vue
  10. 2 2
      src/views/book/courseware/preview/components/article/components/Freewrite.vue
  11. 7 2
      src/views/book/courseware/preview/components/article/components/Notecard.vue
  12. 1 1
      src/views/book/courseware/preview/components/article/components/Strockplay.vue
  13. 1 1
      src/views/book/courseware/preview/components/article/components/Strockplayredline.vue
  14. 2 2
      src/views/book/courseware/preview/components/article/components/Strockred.vue
  15. 6 3
      src/views/book/courseware/preview/components/article/components/Wordcard.vue
  16. 3 3
      src/views/book/courseware/preview/components/article/index.vue
  17. 3 1
      src/views/book/courseware/preview/components/dialogue_article/NormalModelChs.vue
  18. 6 4
      src/views/book/courseware/preview/components/dialogue_article/PhraseModelChs.vue
  19. 6 4
      src/views/book/courseware/preview/components/dialogue_article/WordModelChs.vue
  20. 22 7
      src/views/book/courseware/preview/components/drawing/DrawingPreview.vue
  21. 12 1
      src/views/book/courseware/preview/components/h5_games/H5GamesPreview.vue
  22. 24 2
      src/views/book/courseware/preview/components/image_text/ImageTextPreview.vue
  23. 1 1
      src/views/book/courseware/preview/components/newWord_template/components/Strockplayredline.vue
  24. 1 1
      src/views/book/courseware/preview/components/notes/NotesPreview.vue
  25. 11 2
      src/views/book/courseware/preview/components/table/TablePreview.vue
  26. 8 1
      src/views/book/courseware/preview/components/upload_preview/UploadPreviewPreview.vue
  27. 7 1
      src/views/book/courseware/preview/components/video_interaction/VideoInteractionPreview.vue

+ 50 - 0
src/api/book.js

@@ -282,3 +282,53 @@ export function ApplyBookUnifiedAttrib(data) {
 export function GetBookUnifiedAttrib(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_content_manager-GetBookUnifiedAttrib`, data);
 }
+
+/**
+ *@description 添加我的笔记
+ * @param {object} data  
+ */
+export function AddMyNote(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-AddMyNote`, data);
+}
+/**
+ *@description 更新我的笔记
+ * @param {object} data  
+ */
+export function UpdateMyNote(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-UpdateMyNote`, data);
+}
+/**
+ * @description 得到我的笔记列表
+ * @param {object} data 
+ */
+export function GetMyNoteList(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-GetMyNoteList`, data);
+}
+/**
+ * @description 删除我的笔记
+ * @param {object} data 
+ */
+export function DeleteMyNote(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-DeleteMyNote`, data);
+}
+/**
+ *@description 添加我的收藏
+ * @param {object} data  
+ */
+export function AddMyCollect(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-AddMyCollect`, data);
+}
+/**
+ * @description 得到我的收藏列表
+ * @param {object} data 
+ */
+export function GetMyCollectList(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-GetMyCollectList`, data);
+}
+/**
+ * @description 删除我的收藏
+ * @param {object} data 
+ */
+export function DeleteMyCollect(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-DeleteMyCollect`, data);
+}

+ 269 - 2
src/components/CommonPreview.vue

@@ -81,6 +81,7 @@
             :group-show-all="groupShowAll"
             :group-row-list="content_group_row_list"
             :data="data"
+            :courseware-id="curSelectId"
             :component-list="component_list"
             :background="background"
             :can-remark="isTrue(courseware_info.is_my_audit_task) && isTrue(courseware_info.is_can_add_audit_remark)"
@@ -89,6 +90,8 @@
             :project="project"
             @computeScroll="computeScroll"
             @addRemark="addRemark"
+            @editNote="handEditNote"
+            @saveCollect="saveCollect"
           />
           <div class="preview-right"></div>
         </main>
@@ -175,6 +178,40 @@
             <p v-if="loading">加载中...</p>
             <p v-if="noMore">没有更多了</p>
           </div>
+          <div v-if="curToolbarIcon === 'note'" class="resource_box">
+            <h5>{{ drawerTitle }}</h5>
+            <div style="height: 40px"></div>
+            <ul v-if="allNoteList.length > 0" class="card-box">
+              <li v-for="item in allNoteList" :key="item.id">
+                <span class="el-icon-notebook-2"> 原文</span>
+                <span>{{ item.text }}</span>
+                <el-divider class="mt10" />
+                <span v-html="item.note"></span>
+                <div class="remark-bottom">
+                  <el-button type="text" class="el-icon-edit" @click="handEditNote(item)"> 编辑</el-button>
+                  <el-divider direction="vertical" />
+                  <el-button type="text" class="el-icon-delete" @click="handDelNote(item.id)"> 删除</el-button>
+                  <el-divider direction="vertical" />
+                  <el-button type="text" class="el-icon-place" @click="handLocation(item, 1)"> 定位</el-button>
+                </div>
+              </li>
+            </ul>
+          </div>
+          <div v-if="curToolbarIcon === 'collect'" class="resource_box">
+            <h5>{{ drawerTitle }}</h5>
+            <div style="height: 40px"></div>
+            <ul v-if="allCottectList.length > 0" class="card-box">
+              <li v-for="item in allCottectList" :key="item.id">
+                <span class="el-icon-notebook-2"> 原文</span>
+                <span>{{ item.text }}</span>
+                <div class="remark-bottom">
+                  <el-button type="text" class="el-icon-delete" @click="handDelCollect(item.id)"> 删除</el-button>
+                  <el-divider direction="vertical" />
+                  <el-button type="text" class="el-icon-place" @click="handLocation(item, 2)"> 定位</el-button>
+                </div>
+              </li>
+            </ul>
+          </div>
         </div>
 
         <div class="back-top" @click="backTop">
@@ -215,6 +252,15 @@
         @child-click="handleNodeClick"
       />
     </el-dialog>
+
+    <ExplanatoryNoteDialog
+      ref="explanatoryNote"
+      :open.sync="editDialogOpen"
+      :init-data="oldRichData"
+      title-text="笔记"
+      @confirm="saveNote"
+      @cancel="delNote"
+    />
   </div>
 </template>
 
@@ -226,6 +272,7 @@ import MindMap from '@/components/MindMap.vue';
 import VideoPlay from '@/views/book/courseware/preview/components/common/VideoPlay.vue';
 import AudioPlay from '@/views/book/courseware/preview/components/common/AudioPlay.vue';
 import AuditRemark from '@/components/AuditRemark.vue';
+import ExplanatoryNoteDialog from '@/components/ExplanatoryNoteDialog.vue';
 import * as OpenCC from 'opencc-js';
 
 import {
@@ -244,6 +291,13 @@ import {
   PageQueryBookResourceList,
   GetLanguageTypeList,
   GetBookUnifiedAttrib,
+  GetMyNoteList,
+  DeleteMyNote,
+  AddMyNote,
+  UpdateMyNote,
+  AddMyCollect,
+  GetMyCollectList,
+  DeleteMyCollect,
 } from '@/api/book';
 
 export default {
@@ -255,6 +309,7 @@ export default {
     AudioPlay,
     VideoPlay,
     AuditRemark,
+    ExplanatoryNoteDialog,
   },
   provide() {
     return {
@@ -293,11 +348,11 @@ export default {
       { icon: 'mindmap', title: '思维导图', handle: 'openMindMap', param: {} },
       // { icon: 'knowledge', title: '知识图谱', handle: '', param: {} },
       // { icon: 'totalResources', title: '总资源', handle: '', param: {} },
-      // { icon: 'collect', title: '收藏', handle: '', param: {} },
+      { icon: 'collect', title: '收藏', handle: 'getCollect', param: { type: '3' } },
       { icon: 'audio', title: '音频', handle: 'openDrawer', param: { type: '1' } },
       { icon: 'image', title: '图片', handle: 'openDrawer', param: { type: '0' } },
       { icon: 'video', title: '视频', handle: 'openDrawer', param: { type: '2' } },
-      // { icon: 'note', title: '笔记', handle: '', param: {} },
+      { icon: 'note', title: '笔记', handle: 'getNote', param: { type: '4' } },
       // { icon: 'translate', title: '翻译', handle: '', param: {} },
       // { icon: 'setting', title: '设置', handle: '', param: {} },
     ];
@@ -378,6 +433,11 @@ export default {
         cover_image_file_id: null, // 封面图片ID
         cover_image_file_url: '', // 封面图片URL
       },
+      allNoteList: [],
+      editDialogOpen: false,
+      oldRichData: {},
+      newSelectedInfo: null,
+      allCottectList: [],
     };
   },
   computed: {
@@ -394,6 +454,8 @@ export default {
         0: '图片资源',
         1: '音频资源',
         2: '视频资源',
+        3: '收藏列表',
+        4: '笔记列表',
       };
       return titleMap[this.drawerType] || '资源列表';
     },
@@ -408,6 +470,13 @@ export default {
     isShowAnswer() {
       this.simulateAnswer();
     },
+    curSelectId() {
+      if (this.curToolbarIcon == 'note') {
+        this.getNote();
+      } else if (this.curToolbarIcon == 'collect') {
+        this.getCollect();
+      }
+    },
   },
   created() {
     if (this.id) {
@@ -809,6 +878,187 @@ export default {
         behavior: 'smooth',
       });
     },
+    handLocation(item, type) {
+      if (this.$refs.courserware && this.$refs.courserware.handLocation) {
+        item.type = type;
+        this.$refs.courserware.handLocation(item);
+      }
+    },
+    async getNote() {
+      this.drawerType = 4;
+      this.allNoteList = [];
+      await GetMyNoteList({ courseware_id: this.curSelectId }).then((res) => {
+        if (res.status === 1) {
+          res.note_list.forEach((x) => {
+            if (x.note_desc) {
+              let n = JSON.parse(x.note_desc);
+              let obj = {
+                coursewareId: x.courseware_id,
+                id: x.id,
+                blockId: n.blockId,
+                startIndex: n.startIndex,
+                endIndex: n.endIndex,
+                text: n.text,
+                note: n.note,
+              };
+              this.allNoteList.push(obj);
+            }
+          });
+        }
+      });
+    },
+    async handEditNote(note) {
+      this.oldRichData = {};
+      if (this.allNoteList.length === 0) {
+        await this.getNote();
+      }
+      let old = this.allNoteList.find(
+        (x) =>
+          x.coursewareId === note.coursewareId &&
+          x.blockId === note.blockId &&
+          x.startIndex === note.startIndex &&
+          x.endIndex === note.endIndex,
+      );
+      if (old) {
+        this.oldRichData = old;
+      }
+      this.newSelectedInfo = note;
+      this.editDialogOpen = true;
+    },
+    saveNote(note) {
+      let noteInfo = {
+        blockId: this.newSelectedInfo.blockId,
+        startIndex: this.newSelectedInfo.startIndex,
+        endIndex: this.newSelectedInfo.endIndex,
+        note: note.note,
+        text: this.newSelectedInfo.text,
+      };
+
+      let reqData = {
+        courseware_id: this.newSelectedInfo.coursewareId, // 课件 ID
+        component_id: 'WHOLE',
+        note_desc: JSON.stringify(noteInfo), // 位置描述
+      };
+      if (note.id) {
+        if (!noteInfo.note) {
+          this.delNote(note.id);
+          return;
+        }
+        let upDate = {
+          id: note.id,
+          note_desc: reqData.note_desc,
+        };
+        UpdateMyNote(upDate).then(() => {
+          this.getNote();
+        });
+      } else {
+        AddMyNote(reqData).then(() => {
+          this.getNote();
+        });
+      }
+      this.editDialogOpen = false;
+      this.newSelectedInfo = null;
+      this.selectedInfo = null;
+    },
+    handDelNote(id) {
+      this.$confirm('确定要删除此条笔记吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          this.delNote(id);
+        })
+        .catch(() => {});
+    },
+    delNote(id) {
+      if (!id) {
+        if (!this.oldRichData || !this.oldRichData.id) return;
+        id = this.oldRichData.id;
+      }
+      DeleteMyNote({ id }).then(() => {
+        this.allNoteList = this.allNoteList.filter((x) => x.id !== id);
+      });
+    },
+
+    async getCollect() {
+      this.drawerType = 3;
+      this.allCottectList = [];
+      await GetMyCollectList({ courseware_id: this.curSelectId }).then((res) => {
+        if (res.status === 1) {
+          res.collect_list.forEach((x) => {
+            if (x.collect_desc) {
+              let n = JSON.parse(x.collect_desc);
+              let obj = {
+                coursewareId: x.courseware_id,
+                id: x.id,
+                blockId: n.blockId,
+                startIndex: n.startIndex,
+                endIndex: n.endIndex,
+                text: n.text,
+              };
+              this.allCottectList.push(obj);
+            }
+          });
+        }
+      });
+    },
+    async saveCollect(collect) {
+      if (this.allCottectList.length === 0) {
+        await this.getCollect();
+      }
+
+      let old = this.allCottectList.find(
+        (x) =>
+          x.coursewareId === collect.coursewareId &&
+          x.blockId === collect.blockId &&
+          x.startIndex === collect.startIndex &&
+          x.endIndex === collect.endIndex,
+      );
+      if (old) {
+        this.$message({
+          dangerouslyUseHTMLString: true,
+          message: "<i class='el-icon-check' />已收藏",
+        });
+        return;
+      }
+      this.newSelectedInfo = collect;
+      let collectInfo = {
+        blockId: this.newSelectedInfo.blockId,
+        startIndex: this.newSelectedInfo.startIndex,
+        endIndex: this.newSelectedInfo.endIndex,
+        text: this.newSelectedInfo.text,
+      };
+
+      let reqData = {
+        courseware_id: this.newSelectedInfo.coursewareId, // 课件 ID
+        component_id: 'WHOLE',
+        collect_desc: JSON.stringify(collectInfo), // 位置描述
+      };
+      AddMyCollect(reqData)
+        .then(() => {
+          this.getCollect();
+        })
+        .then(() => {
+          this.$message({
+            dangerouslyUseHTMLString: true,
+            message: "<i class='el-icon-check' />已收藏",
+          });
+        });
+    },
+    handDelCollect(id) {
+      this.$confirm('确定要删除此条收藏吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          DeleteMyCollect({ id: id }).then(() => {
+            this.allCottectList = this.allCottectList.filter((x) => x.id !== id);
+          });
+        })
+        .catch(() => {});
+    },
   },
 };
 </script>
@@ -1237,6 +1487,18 @@ $total-width: $courseware-width + $courseware-left-margin + $courseware-right-ma
             color: #999;
             text-align: center;
           }
+
+          .card-box li {
+            padding: 10px;
+            border-bottom: 1px solid #ccc;
+
+            .el-icon-notebook-2 {
+              display: block;
+              margin-bottom: 4px;
+              font-size: 12px;
+              color: grey;
+            }
+          }
         }
       }
 
@@ -1290,6 +1552,11 @@ $total-width: $courseware-width + $courseware-left-margin + $courseware-right-ma
     }
   }
 }
+
+.mt10 {
+  margin: 10px 0 0 !important;
+  background-color: #eee;
+}
 </style>
 
 <style lang="scss">

+ 6 - 2
src/components/ExplanatoryNoteDialog.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-dialog title="编辑注释" :visible.sync="visible" width="680px" @close="dialogClose()">
+  <el-dialog :title="'编辑'+titleText" :visible.sync="visible" width="680px" @close="dialogClose()" :close-on-click-modal="false">
     <RichText
       v-model="richData.note"
       toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
@@ -33,6 +33,10 @@ export default {
       type: Object,
       default: () => ({}),
     },
+    titleText:{
+      type:String,
+      default:'注释'
+    }
   },
   data() {
     return {
@@ -58,7 +62,7 @@ export default {
       this.visible = false;
     },
     deleteNote() {
-      this.$confirm('确定要删除此条注释吗?', '提示', {
+      this.$confirm('确定要删除此条'+this.titleText+'吗?', '提示', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning',

+ 322 - 1
src/views/book/courseware/preview/CoursewarePreview.vue

@@ -1,5 +1,5 @@
 <template>
-  <div ref="courserware" class="courserware" :style="computedCourserwareStyle()">
+  <div ref="courserware" class="courserware" :style="computedCourserwareStyle()" @mouseup="handleTextSelection">
     <template v-for="(row, i) in data.row_list">
       <div v-show="computedRowVisibility(row.row_id)" :key="i" class="row" :style="getMultipleColStyle(i)">
         <el-checkbox
@@ -68,6 +68,12 @@
         </template>
       </div>
     </template>
+    <!-- 选中文本的工具栏 -->
+    <div v-show="showToolbar" class="contentmenu" :style="contentmenu">
+      <span class="button" @click="setNote"><SvgIcon icon-class="sidebar-text" size="14" /> 笔记</span>
+      <span class="line"></span>
+      <span class="button" @click="setCollect"><SvgIcon icon-class="sidebar-collect" size="14" /> 收藏 </span>
+    </div>
   </div>
 </template>
 
@@ -87,6 +93,10 @@ export default {
       type: Object,
       default: () => ({}),
     },
+    coursewareId: {
+      type: String,
+      default: '',
+    },
     background: {
       type: Object,
       default: () => ({}),
@@ -127,6 +137,7 @@ export default {
   data() {
     return {
       previewComponentList,
+      courseware_id: this.coursewareId,
       bookInfo: {
         theme_color: '',
       },
@@ -138,6 +149,12 @@ export default {
       menuPosition: { x: 0, y: 0, select_node: '' }, // 用于存储菜单的位置
       componentId: '', // 添加批注的组件id
       rowCheckList: {},
+      showToolbar: false,
+      contentmenu: {
+        left: 0,
+        top: 0,
+      },
+      selectedInfo: null,
     };
   },
   watch: {
@@ -152,6 +169,11 @@ export default {
           }, {});
       },
     },
+    coursewareId: {
+      handler(val) {
+        this.courseware_id = val;
+      },
+    },
   },
   mounted() {
     const element = this.$refs.courserware;
@@ -411,6 +433,281 @@ export default {
         });
       });
     },
+    // 处理选中文本
+    handleTextSelection() {
+      this.showToolbar = false;
+      // 延迟处理,确保选择已完成
+      setTimeout(() => {
+        const selection = window.getSelection();
+        if (selection.toString().trim() === '') return null;
+
+        const selectedText = selection.toString().trim();
+        const range = selection.getRangeAt(0);
+        console.info(selectedText, range);
+        this.showToolbar = true;
+        const container = document.querySelector('.courserware');
+        const boxRect = container.getBoundingClientRect();
+        const selectRect = range.getBoundingClientRect();
+        this.contentmenu = {
+          left: `${Math.round(selectRect.left - boxRect.left + selectRect.width / 2 - 63)}px`,
+          top: `${Math.round(selectRect.top - boxRect.top + selectRect.height)}px`, // 向上偏移10px
+        };
+
+        this.selectedInfo = {
+          text: selectedText,
+          range,
+        };
+      }, 100);
+    },
+    // 笔记
+    setNote() {
+      this.showToolbar = false;
+      this.oldRichData = {};
+      let info = this.getSelectionInfo();
+      if (!info) return;
+      info.coursewareId = this.courseware_id;
+
+      this.$emit('editNote', info);
+      this.selectedInfo = null;
+    },
+    // 加入收藏
+    setCollect() {
+      this.showToolbar = false;
+
+      let info = this.getSelectionInfo();
+      if (!info) return;
+      info.coursewareId = this.courseware_id;
+
+      this.$emit('saveCollect', info);
+      this.selectedInfo = null;
+    },
+    // 定位
+    handLocation(item) {
+      this.scrollToDataId(item.blockId);
+    },
+    getSelectionInfo() {
+      if (!this.selectedInfo) return;
+      const range = this.selectedInfo.range;
+      let selectedText = this.selectedInfo.text;
+      if (!selectedText) return null;
+
+      let commonAncestor = range.commonAncestorContainer;
+      if (commonAncestor.nodeType === Node.TEXT_NODE) {
+        commonAncestor = commonAncestor.parentNode;
+      }
+
+      const blockElement = commonAncestor.closest('[data-id]');
+      if (!blockElement) return null;
+
+      const blockId = blockElement.dataset.id;
+
+      // 获取所有汉字元素
+      const charElements = blockElement.querySelectorAll('.py-char,.rich-text,.NNPE-chs');
+
+      // 构建包含位置信息的文本数组
+      const textFragments = Array.from(charElements)
+        .map((el, index) => {
+          let text = '';
+          if (el.classList.contains('rich-text')) {
+            const pElements = Array.from(el.querySelectorAll('p'));
+            text = pElements.map((p) => p.textContent.trim()).join('');
+          } else if (el.classList.contains('NNPE-chs')) {
+            const spanElements = Array.from(el.querySelectorAll('span'));
+            spanElements.push(el);
+            text = spanElements.map((span) => span.textContent.trim()).join('');
+          } else {
+            text = el.textContent.trim();
+          }
+
+          // 过滤掉拼音和空文本
+          if (!text || /^[a-zāáǎàōóǒòēéěèīíǐìūúǔùǖǘǚǜü]+$/i.test(text)) {
+            return { text: '', element: el, index };
+          }
+
+          return { text, element: el, index };
+        })
+        .filter((fragment) => fragment.text);
+
+      // 获取完整的纯文本
+      const fullText = textFragments.map((f) => f.text).join('');
+
+      // 清理选中文本
+      let cleanSelectedText = selectedText.replace(/\n/g, '').trim();
+      cleanSelectedText = cleanSelectedText.replace(/[a-zāáǎàōóǒòēéěèīíǐìūúǔùǖǘǚǜü]/gi, '').trim();
+
+      if (!cleanSelectedText) return null;
+
+      // 方案1A:使用Range的边界点精确定位
+      try {
+        const startContainer = range.startContainer;
+        const endContainer = range.endContainer;
+        const startOffset = range.startOffset;
+        const endOffset = range.endOffset;
+
+        // 找到选择开始的元素在textFragments中的位置
+        let startFragmentIndex = -1;
+        let cumulativeLength = 0;
+        let startIndexInFullText = -1;
+
+        for (let i = 0; i < textFragments.length; i++) {
+          const fragment = textFragments[i];
+
+          // 检查这个元素是否包含选择起点
+          if (fragment.element.contains(startContainer) || fragment.element === startContainer) {
+            // 计算在这个元素内的起始位置
+            if (startContainer.nodeType === Node.TEXT_NODE) {
+              // 如果是文本节点,需要计算在父元素中的偏移
+              const elementText = fragment.text;
+              startFragmentIndex = i;
+              startIndexInFullText = cumulativeLength + Math.min(startOffset, elementText.length);
+              break;
+            } else {
+              // 如果是元素节点,从0开始
+              startFragmentIndex = i;
+              startIndexInFullText = cumulativeLength;
+              break;
+            }
+          }
+          cumulativeLength += fragment.text.length;
+        }
+
+        if (startIndexInFullText === -1) {
+          // 如果精确定位失败,回退到文本匹配(但使用更智能的匹配)
+          return this.fallbackToTextMatch(fullText, cleanSelectedText, range, textFragments);
+        }
+
+        const endIndexInFullText = startIndexInFullText + cleanSelectedText.length;
+
+        return {
+          blockId,
+          text: cleanSelectedText,
+          startIndex: startIndexInFullText,
+          endIndex: endIndexInFullText,
+          fullText,
+        };
+      } catch (error) {
+        console.warn('精确位置计算失败,使用备选方案:', error);
+        return this.fallbackToTextMatch(fullText, cleanSelectedText, range, textFragments);
+      }
+    },
+
+    // 备选方案:基于DOM位置的智能匹配
+    fallbackToTextMatch(fullText, selectedText, range, textFragments) {
+      // 获取选择范围的近似位置
+      const rangeRect = range.getBoundingClientRect();
+
+      // 找到最接近选择中心的文本片段
+      let closestFragment = null;
+      let minDistance = Infinity;
+
+      textFragments.forEach((fragment) => {
+        const rect = fragment.element.getBoundingClientRect();
+        if (rect.width > 0 && rect.height > 0) {
+          // 确保元素可见
+          const centerX = rect.left + rect.width / 2;
+          const centerY = rect.top + rect.height / 2;
+          const rangeCenterX = rangeRect.left + rangeRect.width / 2;
+          const rangeCenterY = rangeRect.top + rangeRect.height / 2;
+
+          const distance = Math.sqrt(Math.pow(centerX - rangeCenterX, 2) + Math.pow(centerY - rangeCenterY, 2));
+
+          if (distance < minDistance) {
+            minDistance = distance;
+            closestFragment = fragment;
+          }
+        }
+      });
+
+      if (closestFragment) {
+        // 从最近的片段开始向前后搜索匹配
+        const fragmentIndex = textFragments.indexOf(closestFragment);
+        let cumulativeLength = 0;
+
+        // 计算到当前片段的累计长度
+        for (let i = 0; i < fragmentIndex; i++) {
+          cumulativeLength += textFragments[i].text.length;
+        }
+
+        // 在当前片段附近搜索匹配
+        const searchStart = Math.max(0, cumulativeLength - selectedText.length * 3);
+        const searchEnd = Math.min(
+          fullText.length,
+          cumulativeLength + closestFragment.text.length + selectedText.length * 3,
+        );
+
+        const searchArea = fullText.substring(searchStart, searchEnd);
+        const localIndex = searchArea.indexOf(selectedText);
+
+        if (localIndex !== -1) {
+          return {
+            startIndex: searchStart + localIndex,
+            endIndex: searchStart + localIndex + selectedText.length,
+            text: selectedText,
+            fullText,
+          };
+        }
+      }
+
+      // 最终回退:使用所有匹配位置,选择最合理的一个
+      const allMatches = [];
+      let searchIndex = 0;
+
+      while ((searchIndex = fullText.indexOf(selectedText, searchIndex)) !== -1) {
+        allMatches.push(searchIndex);
+        searchIndex += selectedText.length;
+      }
+
+      if (allMatches.length === 1) {
+        return {
+          startIndex: allMatches[0],
+          endIndex: allMatches[0] + selectedText.length,
+          text: selectedText,
+          fullText,
+        };
+      } else if (allMatches.length > 1) {
+        // 如果有多个匹配,选择位置最接近选择中心的
+        if (closestFragment) {
+          let cumulativeLength = 0;
+          let fragmentStartIndex = 0;
+
+          for (let i = 0; i < textFragments.length; i++) {
+            if (textFragments[i] === closestFragment) {
+              fragmentStartIndex = cumulativeLength;
+              break;
+            }
+            cumulativeLength += textFragments[i].text.length;
+          }
+
+          // 选择最接近当前片段起始位置的匹配
+          const bestMatch = allMatches.reduce((best, current) => {
+            return Math.abs(current - fragmentStartIndex) < Math.abs(best - fragmentStartIndex) ? current : best;
+          });
+
+          return {
+            startIndex: bestMatch,
+            endIndex: bestMatch + selectedText.length,
+            text: selectedText,
+            fullText,
+          };
+        }
+      }
+
+      return null;
+    },
+    scrollToDataId(dataId, offset) {
+      if (!offset) offset = 0;
+      const element = document.querySelector(`div[data-id="${dataId}"]`);
+      if (element) {
+        const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
+        const offsetPosition = elementPosition - offset;
+
+        element.scrollIntoView({
+          behavior: 'smooth', // 滚动行为:'auto' | 'smooth'
+          block: 'center', // 垂直对齐:'start' | 'center' | 'end' | 'nearest'
+          inline: 'nearest', // 水平对齐:'start' | 'center' | 'end' | 'nearest'
+        });
+      }
+    },
   },
 };
 </script>
@@ -483,5 +780,29 @@ export default {
     background: url('../../../../assets/icon-publish.png') left center no-repeat;
     background-size: 24px;
   }
+
+  .contentmenu {
+    position: absolute;
+    z-index: 999;
+    display: flex;
+    column-gap: 4px;
+    align-items: center;
+    padding: 8px;
+    font-size: 14px;
+    color: #000;
+    background-color: #e7e7e7;
+    border-radius: 4px;
+    box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 30%);
+
+    .svg-icon,
+    .button {
+      cursor: pointer;
+    }
+
+    .line {
+      min-height: 16px;
+      margin: 0 4px;
+    }
+  }
 }
 </style>

+ 2 - 1
src/views/book/courseware/preview/components/article/NormalModelChs.vue

@@ -1179,9 +1179,10 @@
         :style="{
           marginLeft: windowWidth > 642 ? '-321px' : '0px',
           left: windowWidth > 642 ? '' : '0px',
+          width: isMobile ? '100%' : '',
         }"
       >
-        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" />
+        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" :isMobile="isMobile" />
       </div>
     </template>
   </div>

+ 4 - 2
src/views/book/courseware/preview/components/article/PhraseModelChs.vue

@@ -867,6 +867,7 @@
           :bg="activeWord ? activeWord.bg : null"
           :ed="activeWord ? activeWord.ed : null"
           :attrib="attrib"
+          :isMobile="isMobile"
           @changeCurQue="changeCurQue"
         />
       </div>
@@ -878,9 +879,10 @@
         :style="{
           marginLeft: windowWidth > 642 ? '-321px' : '0px',
           left: windowWidth > 642 ? '' : '0px',
+          width: isMobile ? '100%' : '',
         }"
       >
-        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" />
+        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" :isMobile="isMobile" />
       </div>
     </template>
   </div>
@@ -890,7 +892,6 @@
 import AudioLine from '../voice_matrix/components/AudioLine.vue';
 import Wordcard from './components/Wordcard.vue'; // 卡片
 import Notecard from './components/Notecard.vue'; // 注释
-import $ from 'jquery';
 export default {
   name: 'PhraseModelChs',
   components: {
@@ -911,6 +912,7 @@ export default {
     'colLength',
     'multilingual',
     'attrib',
+    'isMobile',
   ],
   data() {
     return {

+ 2 - 1
src/views/book/courseware/preview/components/article/Practicechs.vue

@@ -1237,7 +1237,8 @@ export default {
 
       .luyin-box {
         width: 280px;
-        max-width: 280px;
+
+        // max-width: 280px;
       }
 
       .compare-box {

+ 5 - 1
src/views/book/courseware/preview/components/article/Voicefullscreen.vue

@@ -515,6 +515,8 @@
           :bg="wordbgs"
           :ed="wordeds"
           :attrib="attrib"
+          :isMobile="isMobile"
+          :isFull="true"
         />
       </div>
     </template>
@@ -543,9 +545,10 @@
         :style="{
           marginLeft: windowWidth > 642 ? '-321px' : '0px',
           left: windowWidth > 642 ? '' : '0px',
+          width: isMobile ? '100%' : '',
         }"
       >
-        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" />
+        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" :isMobile="isMobile" />
       </div>
     </template>
   </div>
@@ -583,6 +586,7 @@ export default {
     'TaskModel',
     'NpcNewWordMp3',
     'attrib',
+    'isMobile',
   ],
   data() {
     return {

+ 1 - 0
src/views/book/courseware/preview/components/article/WordModelChs.vue

@@ -824,6 +824,7 @@
           :TaskModel="TaskModel"
           :write-list="curQue.Bookanswer.writeModel"
           :attrib="attrib"
+          :isMobile="isMobile"
           @changeCurQue="changeCurQue"
         />
       </div>

+ 2 - 2
src/views/book/courseware/preview/components/article/components/Freewrite.vue

@@ -187,7 +187,7 @@ export default {
         .generate()
         .then((res) => {
           let Book_img = res.replace('data:image/png;base64,', '');
-          let write_img = `data:image/png;base64,${  Book_img}`;
+          let write_img = `data:image/png;base64,${Book_img}`;
           let answer = {};
           answer = {
             hz,
@@ -315,7 +315,7 @@ export default {
     margin: 0 auto;
 
     .character-target-div {
-      z-index: 99999;
+      z-index: 1;
       display: flex;
       align-items: center;
       justify-content: center;

+ 7 - 2
src/views/book/courseware/preview/components/article/components/Notecard.vue

@@ -1,6 +1,6 @@
 <!--  -->
 <template>
-  <div v-if="item" class="NoteCard">
+  <div v-if="item" class="NoteCard" :class="[isMobile ? 'NoteCard-phone' : '']">
     <div class="closeBox">
       <i class="el-icon-close" @click="changeCard(false)"></i>
     </div>
@@ -36,7 +36,7 @@
 <script>
 export default {
   components: {},
-  props: ['item', 'changeCard', 'attrib'],
+  props: ['item', 'changeCard', 'attrib', 'isMobile'],
   data() {
     return {};
   },
@@ -150,5 +150,10 @@ export default {
   :deep p {
     margin: 0;
   }
+
+  &-phone {
+    width: 100%;
+    margin: 0;
+  }
 }
 </style>

+ 1 - 1
src/views/book/courseware/preview/components/article/components/Strockplay.vue

@@ -86,7 +86,7 @@ export default {
 }
 
 .character-target-div {
-  z-index: 99999;
+  z-index: 1;
   display: flex;
   align-items: center;
   justify-content: center;

+ 1 - 1
src/views/book/courseware/preview/components/article/components/Strockplayredline.vue

@@ -103,7 +103,7 @@ export default {
 }
 
 .character-target-div {
-  z-index: 99999;
+  z-index: 1;
   display: flex;
   align-items: center;
   justify-content: center;

+ 2 - 2
src/views/book/courseware/preview/components/article/components/Strockred.vue

@@ -63,7 +63,7 @@ export default {
     initHanziwrite() {
       let _this = this;
       let options = {
-        charDataLoader (char, onComplete) {
+        charDataLoader(char, onComplete) {
           let MethodName = 'hz_resource_manager-GetHZStrokesContent';
           let data = {
             hz: char,
@@ -108,7 +108,7 @@ export default {
 
   //chinawrite220.png
   .character-target-div {
-    z-index: 99999;
+    z-index: 1;
     display: flex;
     align-items: center;
     justify-content: center;

+ 6 - 3
src/views/book/courseware/preview/components/article/components/Wordcard.vue

@@ -239,6 +239,7 @@ export default {
     'bg',
     'ed',
     'attrib',
+    'isMobile',
   ],
   data() {
     return {
@@ -643,12 +644,14 @@ export default {
 
     > span {
       width: 100%;
-      overflow: hidden;
+
+      // overflow: hidden;
       font-size: 14px;
       line-height: 22px;
       color: rgba(0, 0, 0, 85%);
-      text-overflow: ellipsis;
-      white-space: nowrap;
+
+      // text-overflow: ellipsis;
+      // white-space: nowrap;
 
       :deep p {
         margin: 0;

+ 3 - 3
src/views/book/courseware/preview/components/article/index.vue

@@ -214,17 +214,17 @@
       <template
         v-if="data.new_word_list && data.new_word_list.new_word_list && data.new_word_list.new_word_list.length > 0"
       >
-        <NewWordPreview :new-data="data.new_word_list" />
+        <NewWordPreview :new-data="data.new_word_list" :isMobile="isMobile" />
       </template>
       <template
         v-if="
           data.other_word_list && data.other_word_list.new_word_list && data.other_word_list.new_word_list.length > 0
         "
       >
-        <NewWordPreview :new-data="data.other_word_list" />
+        <NewWordPreview :new-data="data.other_word_list" :isMobile="isMobile" />
       </template>
       <template v-if="data.notes_list && data.notes_list.option && data.notes_list.option.length > 0">
-        <NotesPreview :notes-data="data.notes_list" />
+        <NotesPreview :notes-data="data.notes_list" :isMobile="isMobile" />
       </template>
     </div>
   </div>

+ 3 - 1
src/views/book/courseware/preview/components/dialogue_article/NormalModelChs.vue

@@ -690,9 +690,10 @@
         :style="{
           marginLeft: windowWidth > 642 ? '-321px' : '0px',
           left: windowWidth > 642 ? '' : '0px',
+          width: isMobile ? '100%' : '',
         }"
       >
-        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" />
+        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" :isMobile="isMobile" />
       </div>
     </template>
   </div>
@@ -751,6 +752,7 @@ export default {
     'isPhone',
     'multilingual',
     'attrib',
+    'isMobile',
   ],
   data() {
     return {

+ 6 - 4
src/views/book/courseware/preview/components/dialogue_article/PhraseModelChs.vue

@@ -18,7 +18,7 @@
             :mp3="curQue.mp3_list[0].url"
             :get-cur-time="getCurTime"
             :mp3-source="curQue.mp3_list[0].source"
-            :width="colLength == 2 ? 200 : isPhone ? 200 : 790"
+            :width="colLength == 2 ? 200 : isMobile ? 200 : 790"
             :attrib="attrib"
           />
         </template>
@@ -455,7 +455,7 @@
             :mp3="curQue.mp3_list[0].url"
             :get-cur-time="getCurTime"
             :mp3-source="curQue.mp3_list[0].source"
-            :width="colLength == 2 ? 200 : isPhone ? 200 : 790"
+            :width="colLength == 2 ? 200 : isMobile ? 200 : 790"
             :attrib="attrib"
           />
         </template>
@@ -504,6 +504,7 @@
           :bg="wordbgs"
           :ed="wordeds"
           :attrib="attrib"
+          :isMobile="isMobile"
           @changeCurQue="changeCurQue"
         />
       </div>
@@ -515,9 +516,10 @@
         :style="{
           marginLeft: windowWidth > 642 ? '-321px' : '0px',
           left: windowWidth > 642 ? '' : '0px',
+          width: isMobile ? '100%' : '',
         }"
       >
-        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" />
+        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" :isMobile="isMobile" />
       </div>
     </template>
   </div>
@@ -578,7 +580,7 @@ export default {
     'TaskModel',
     'colLength',
     'NpcNewWordMp3',
-    'isPhone',
+    'isMobile',
     'multilingual',
   ],
   data() {

+ 6 - 4
src/views/book/courseware/preview/components/dialogue_article/WordModelChs.vue

@@ -18,7 +18,7 @@
             :mp3="curQue.mp3_list[0].url"
             :get-cur-time="getCurTime"
             :mp3-source="curQue.mp3_list[0].source"
-            :width="colLength == 2 ? 200 : isPhone ? 200 : 790"
+            :width="colLength == 2 ? 200 : isMobile ? 200 : 790"
             :attrib="attrib"
           />
         </template>
@@ -431,7 +431,7 @@
             :mp3="curQue.mp3_list[0].url"
             :get-cur-time="getCurTime"
             :mp3-source="curQue.mp3_list[0].source"
-            :width="colLength == 2 ? 200 : isPhone ? 200 : 790"
+            :width="colLength == 2 ? 200 : isMobile ? 200 : 790"
           />
         </template>
       </div>
@@ -476,6 +476,7 @@
           :TaskModel="TaskModel"
           :write-list="curQue.Bookanswer.writeModel"
           :attrib="attrib"
+          :isMobile="isMobile"
           @changeCurQue="changeCurQue"
         />
       </div>
@@ -487,9 +488,10 @@
         :style="{
           marginLeft: windowWidth > 642 ? '-321px' : '0px',
           left: windowWidth > 642 ? '' : '0px',
+          width: isMobile ? '100%' : '',
         }"
       >
-        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" />
+        <Notecard :item="curNoteCon" :change-card="changeCard" :attrib="attrib" :isMobile="isMobile" />
       </div>
     </template>
   </div>
@@ -548,7 +550,7 @@ export default {
     'config',
     'TaskModel',
     'colLength',
-    'isPhone',
+    'isMobile',
     'multilingual',
   ],
   data() {

+ 22 - 7
src/views/book/courseware/preview/components/drawing/DrawingPreview.vue

@@ -1,14 +1,14 @@
 <!-- eslint-disable vue/no-v-html -->
 <template>
-  <div class="imageText-preview" :style="getAreaStyle()">
+  <div class="drawimg-preview" :style="getAreaStyle()">
     <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
 
     <div
       class="img-box"
       :style="{
         background: image_url ? '' : '#DCDFE6',
-        width: data.image_width + 'px',
-        height: data.image_height + 'px',
+        width: isMobile ? '100%' : data.image_width + 'px',
+        height: isMobile ? mobileHeight + 'px' : data.image_height + 'px',
         border: '1px dotted #DCDFE6',
       }"
     >
@@ -21,7 +21,10 @@
           :src="image_url"
           draggable="false"
           alt="背景图"
-          :style="{ width: `${data.imgData.width}px`, height: `${data.imgData.height}px` }"
+          :style="{
+            width: isMobile ? '100%' : `${data.imgData.width}px`,
+            height: isMobile ? mobileHeight + 'px' : `${data.imgData.height}px`,
+          }"
         />
       </div>
       <!-- 如果是查看答案模式 v-if 下面画画的vue-esign不显示 -->
@@ -37,8 +40,8 @@
       /> -->
       <VueSignaturePad
         class="esign-canvas"
-        :width="data.image_width + 'px'"
-        :height="data.image_height + 'px'"
+        :width="isMobile ? '100%' : data.image_width + 'px'"
+        :height="isMobile ? mobileHeight + 'px' : data.image_height + 'px'"
         ref="signaturePad"
         :options="options"
       />
@@ -67,7 +70,12 @@ import { GetFileURLMap } from '@/api/app';
 import vueEsign from 'vue-esign';
 export default {
   name: 'DrawingPreview',
-
+  props: {
+    isMobile: {
+      type: Boolean,
+      default: false,
+    },
+  },
   components: { vueEsign },
   mixins: [PreviewMixin],
   data() {
@@ -99,6 +107,7 @@ export default {
       isActive3: false,
       imgSrc: '',
       signData: null,
+      mobileHeight: 0,
     };
   },
   created() {
@@ -168,6 +177,12 @@ export default {
         //   this.image_url = url_map[item.file_id];
         // });
       });
+      if (this.isMobile) {
+        setTimeout(() => {
+          let totalWidth = document.querySelector('.drawimg-preview').offsetWidth;
+          this.mobileHeight = (totalWidth / this.data.image_width) * this.data.image_height;
+        }, 50);
+      }
     },
     // 保存图片
     handleGenerate() {

+ 12 - 1
src/views/book/courseware/preview/components/h5_games/H5GamesPreview.vue

@@ -16,7 +16,12 @@
         @click="toggleFullScreen"
         >{{ full_type ? '退出全屏' : '进入全屏' }}</el-button
       >
-      <iframe :src="games_url" width="100%" :height="full_type ? '100%' : '580px'" style="border: none"></iframe>
+      <iframe
+        :src="games_url"
+        width="100%"
+        :height="full_type ? '100%' : isMobile ? '400px' : '580px'"
+        style="border: none"
+      ></iframe>
     </div>
   </div>
 </template>
@@ -27,6 +32,12 @@ import { getH5GamesData } from '@/views/book/courseware/data/h5Games';
 import { H5StartupFile } from '@/api/app';
 export default {
   name: 'H5GamesPreview',
+  props: {
+    isMobile: {
+      type: Boolean,
+      default: false,
+    },
+  },
 
   components: {},
   mixins: [PreviewMixin],

+ 24 - 2
src/views/book/courseware/preview/components/image_text/ImageTextPreview.vue

@@ -95,8 +95,9 @@
       :visible.sync="mageazineDetailShow"
       :show-close="false"
       :close-on-click-modal="false"
-      width="80%"
+      :width="isMobile ? '100%' : '80%'"
       class="login-dialog magazine-detail-dialog"
+      :class="[isMobile ? 'magazine-detail-dialog-phone' : '']"
       :modal="false"
     >
       <magazine-sentence
@@ -123,7 +124,12 @@ import { GetFileURLMap } from '@/api/app';
 import MagazineSentence from './components/MagazineSentence.vue';
 export default {
   name: 'ImageTextPreview',
-
+  props: {
+    isMobile: {
+      type: Boolean,
+      default: false,
+    },
+  },
   components: { AudioLine, MagazineSentence },
   mixins: [PreviewMixin],
   data() {
@@ -252,6 +258,11 @@ export default {
 <style lang="scss" scoped>
 @use '@/styles/mixin.scss' as *;
 
+.imageText-preview {
+  max-width: 100%;
+  overflow: auto;
+}
+
 .position-item {
   position: absolute;
   z-index: 1;
@@ -307,6 +318,11 @@ export default {
     margin: 24px 0;
   }
 }
+
+:deep .el-slider {
+  flex: 1;
+  width: auto !important;
+}
 </style>
 <style lang="scss">
 .magazine-detail-dialog {
@@ -327,5 +343,11 @@ export default {
       0 16px 24px 2px rgba(0, 0, 0, 4%),
       0 8px 10px -5px rgba(0, 0, 0, 8%);
   }
+
+  &-phone {
+    .el-dialog {
+      margin-left: -50%;
+    }
+  }
 }
 </style>

+ 1 - 1
src/views/book/courseware/preview/components/newWord_template/components/Strockplayredline.vue

@@ -178,7 +178,7 @@ export default {
 
 .character-target-div {
   position: absolute;
-  z-index: 999;
+  z-index: 1;
   display: flex;
   align-items: center;
   justify-content: center;

+ 1 - 1
src/views/book/courseware/preview/components/notes/NotesPreview.vue

@@ -170,7 +170,7 @@ export default {
   name: 'NotesPreview',
   components: {},
   mixins: [PreviewMixin],
-  props: ['notesData'],
+  props: ['notesData', 'isMobile'],
   data() {
     return {
       data: this.notesData ? this.notesData : getNotesData(),

+ 11 - 2
src/views/book/courseware/preview/components/table/TablePreview.vue

@@ -7,13 +7,15 @@
       <div
         class="table-box"
         :style="{
-          width: data.property.width + 'px',
+          width: isMobile ? '100%' : data.property.width + 'px',
+
           height: data.property.height + 'px',
         }"
       >
         <table
           :style="{
-            width: table_width + 'px',
+            width: isMobile ? '100%' : table_width + 'px',
+
             height: data.property.height + 'px',
           }"
         >
@@ -240,6 +242,13 @@ export default {
   name: 'TablePreview',
   components: { SoundRecordBox, WriteDialog },
   mixins: [PreviewMixin],
+  props: {
+    isMobile: {
+      type: Boolean,
+      default: false,
+    },
+  },
+
   data() {
     return {
       data: getTableData(),

+ 8 - 1
src/views/book/courseware/preview/components/upload_preview/UploadPreviewPreview.vue

@@ -88,7 +88,7 @@
       :modal-append-to-body="true"
       :append-to-body="true"
       :lock-scroll="true"
-      width="80%"
+      :width="isMobile ? '100%' : '80%'"
       top="0"
     >
       <iframe v-if="visible" :src="newpath" width="100%" :height="iframeHeight" frameborder="0"></iframe>
@@ -107,6 +107,13 @@ import { getUploadPreviewData } from '@/views/book/courseware/data/uploadPreview
 export default {
   name: 'UploadPreviewPreview',
   mixins: [PreviewMixin],
+  props: {
+    isMobile: {
+      type: Boolean,
+      default: false,
+    },
+  },
+
   data() {
     return {
       data: getUploadPreviewData(),

+ 7 - 1
src/views/book/courseware/preview/components/video_interaction/VideoInteractionPreview.vue

@@ -46,7 +46,7 @@
       :modal-append-to-body="true"
       :append-to-body="true"
       :lock-scroll="true"
-      width="80%"
+      :width="isMobile ? '100%' : '80%'"
       @close="handleClose"
     >
       <Report
@@ -68,6 +68,12 @@ import { getVideoInteractionData } from '@/views/book/courseware/data/videoInter
 import { getConfig } from '@/utils/auth';
 export default {
   name: 'VideoInteractionPreview',
+  props: {
+    isMobile: {
+      type: Boolean,
+      default: false,
+    },
+  },
 
   components: { ExercisePreview, Report },
   mixins: [PreviewMixin],