瀏覽代碼

字间距,多个图片可调整大小

zq 2 周之前
父節點
當前提交
e771c55356

+ 56 - 1
src/components/RichText.vue

@@ -92,7 +92,7 @@ export default {
       type: [String, Boolean],
       /* eslint-disable max-len */
       default:
-        'fontselect fontsizeselect forecolor backcolor lineheight paragraphSpacing  indent outdent customUnderline bold italic strikethrough alignleft aligncenter alignright bullist numlist dotEmphasis image media link blockquote hr mathjax',
+        'fontselect fontsizeselect forecolor backcolor lineheight paragraphSpacing letterSpacing indent outdent customUnderline bold italic strikethrough alignleft aligncenter alignright bullist numlist dotEmphasis image media link blockquote hr mathjax',
     },
     wordlimitNum: {
       type: [Number, Boolean],
@@ -186,6 +186,7 @@ export default {
         toolbar: this.toolbar, // 工具栏
         lineheight_formats: '0.5 1.0 1.2 1.5 2.0 2.5 3.0', // 行高选项(倍数)
         paragraphheight_formats: [1.0, 1.2, 1.5, 2.0, 2.5, 3.0], // 段落间距
+        letterSpacing_formats: [1.0, 1.2, 1.5, 2.0, 2.5, 3.0], // 字间距选项
         contextmenu: false, // 右键菜单
         menubar: false, // 菜单栏
         branding: false, // 品牌
@@ -212,6 +213,15 @@ export default {
                 styles: { 'margin-bottom': `${config}em` },
               });
             });
+            this.init.letterSpacing_formats.forEach((config) => {
+              const formatName = `letterSpacing${config}_em`;
+              editor.formatter.register(formatName, {
+                inline: 'span',
+                styles: { 'letter-spacing': `${config}em` },
+                wrapper: true,
+                remove_similar: true,
+              });
+            });
 
             if (!editor.formatter.has('emphasisDot')) {
               editor.formatter.register('emphasisDot', {
@@ -392,6 +402,51 @@ export default {
             },
           });
 
+          // 添加字间距下拉菜单
+          editor.ui.registry.addMenuButton('letterSpacing', {
+            icon: 'border-width',
+            // text: '字间距',
+            tooltip: '字间距',
+            fetch: (callback) => {
+              const items = [];
+
+              // 动态生成菜单项
+              this.init.letterSpacing_formats.forEach((config) => {
+                const formatName = `letterSpacing${config}_em`;
+                items.push({
+                  type: 'menuitem',
+                  text: `${config}`,
+                  onAction: () => {
+                    // 先清除其他字间距格式
+                    this.init.letterSpacing_formats.forEach((cfg) => {
+                      const fmtName = `letterSpacing${cfg}_em`;
+                      editor.formatter.remove(fmtName);
+                    });
+                    // 应用当前选择的字间距
+                    editor.formatter.apply(formatName);
+                  },
+                });
+              });
+
+              // 添加清除字间距选项
+              items.push({
+                type: 'separator', // 分隔线
+              });
+              items.push({
+                type: 'menuitem',
+                text: '清除字间距',
+                onAction: () => {
+                  // 清除所有字间距格式
+                  this.init.letterSpacing_formats.forEach((config) => {
+                    const formatName = `letterSpacing${config}_em`;
+                    editor.formatter.remove(formatName);
+                  });
+                },
+              });
+
+              callback(items);
+            },
+          });
           // 添加 添加着重点 按钮
           editor.ui.registry.addButton('dotEmphasis', {
             text: '●',

+ 12 - 0
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -112,6 +112,7 @@
       :courseware-id="courseware_id"
       :row-list="data.row_list"
       @computedMoveData="computedMoveData"
+      @handleHeightChange="handleHeightChange"
     />
   </main>
 </template>
@@ -365,6 +366,17 @@ export default {
     document.removeEventListener('mouseup', this.dragEnd);
   },
   methods: {
+    handleHeightChange(id, newHeight) {
+      this.data.row_list.forEach((row) => {
+        row.col_list.forEach((col) => {
+          col.grid_list.forEach((grid) => {
+            if (grid.id === id) {
+              grid.height = newHeight;
+            }
+          });
+        });
+      });
+    },
     changeData() {
       this.$emit('changeData');
     },

+ 4 - 0
src/views/book/courseware/create/components/PreviewEdit.vue

@@ -30,6 +30,7 @@
                 :id="grid.id"
                 ref="preview"
                 :key="`preview-${grid.id}`"
+                @handleHeightChange="handleHeightChange"
                 :courseware-id="coursewareId"
                 type="edit"
                 :class="[grid.id]"
@@ -141,6 +142,9 @@ export default {
     }
   },
   methods: {
+    handleHeightChange(id, newHeight) {
+      this.$emit('handleHeightChange', id, newHeight);
+    },
     getMultipleColStyle(i) {
       let row = this.rowList[i];
       let col = row.col_list;

+ 11 - 1
src/views/book/courseware/create/components/base/common/UploadFile.vue

@@ -3,6 +3,7 @@
     <div class="file-area">
       <span class="label-text">{{ labelText }}</span>
       <div class="upload-box">
+        <!-- style="pointer-events: none;" -->
         <el-upload
           ref="upload"
           class="file-uploader"
@@ -18,7 +19,7 @@
         >
           <el-button>{{ type === 'h5_games' ? '选择Zip压缩包或单个html文件' : '选取' + labelText + '文件' }}</el-button>
         </el-upload>
-        <el-button size="small" type="primary" @click="uploadFiles"> 上传 </el-button>
+        <el-button size="small" type="primary" @click="selectAndUpload">本地上传</el-button>
         <el-button v-if="isEnable(projectResourcePopedom.is_can_use)" size="small" type="primary" @click="useResource">
           使用资源
         </el-button>
@@ -258,6 +259,13 @@ export default {
     },
   },
   methods: {
+    selectAndUpload() {
+      const uploadInput = this.$refs.upload.$el.querySelector('input[type="file"]');
+      if (uploadInput) {
+        uploadInput.value = '';
+        uploadInput.click();
+      }
+    },
     // 显示自定义样式文件列表
     onFileChange(file, fileList) {
       this.afterSelectFile(file);
@@ -279,6 +287,8 @@ export default {
         return;
       }
       this.content.file_list = [...this.content.file_list, ...files];
+      // 选中文件后自动上传
+      this.uploadFiles();
     },
 
     handleExceed(files, fileList) {

+ 14 - 4
src/views/book/courseware/create/components/base/picture/Picture.vue

@@ -39,11 +39,21 @@ export default {
     };
   },
   watch: {
-    'data.file_list': {
-      handler(fileList) {
-        if (fileList.length === 1) {
+    // 'data.file_list': {
+    //   handler(fileList) {
+    //     if (fileList.length === 1) {
+    //       this.data.min_height = '144';
+    //     } else if (fileList.length > 1) {
+    //       this.data.min_height = '336';
+    //     }
+    //   },
+    //   immediate: true,
+    // },
+    'data.property.view_method': {
+      handler(view_method) {
+        if (view_method === 'independent') {
           this.data.min_height = '144';
-        } else if (fileList.length > 1) {
+        } else {
           this.data.min_height = '336';
         }
       },

+ 5 - 1
src/views/book/courseware/create/components/base/rich_text/RichText.vue

@@ -62,6 +62,7 @@
           :id="richId + '_pinyin_text'"
           ref="PinyinText"
           :paragraph-list="data.paragraph_list"
+          :paragraph-version="paragraphVersion"
           :pinyin-position="data.property.pinyin_position"
           :pinyin-overall-position="data.property.pinyin_overall_position"
           :pinyin-size="data?.unified_attrib?.pinyin_size"
@@ -111,6 +112,7 @@ export default {
       showWordFlag: false,
       wordData: {},
       inited: false,
+      paragraphVersion: 0,
     };
   },
   watch: {
@@ -243,7 +245,9 @@ export default {
               }),
             ),
           );
-          this.data.paragraph_list = mergedData; // 取出合并后的数组
+          //this.data.paragraph_list = mergedData; // 取出合并后的数组
+          this.$set(this.data, 'paragraph_list', mergedData);
+          this.paragraphVersion++;
           this.parseFClist();
         }
       });

+ 130 - 63
src/views/book/courseware/preview/components/picture/PicturePreview.vue

@@ -1,62 +1,57 @@
 <template>
   <div ref="pictureArea" class="picture-preview" :style="getAreaStyle()">
     <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
+
     <div ref="pictureAreaBox" class="main">
       <div class="view-area">
-        <template v-if="isMore">
-          <div class="picture-area">
-            <!-- 播放列表 -->
-            <template v-if="'list' === data.property.view_method">
-              <el-carousel
-                ref="pictureCarousel"
-                class="view-list"
-                indicator-position="none"
-                :autoplay="false"
-                :style="{ height: elementHeight - 144 - 32 + 'px' }"
-                @change="handleChange"
-              >
-                <el-carousel-item v-for="(file, i) in data.file_list" :key="i">
-                  <el-image
-                    :id="file.file_id"
-                    :src="file.file_url"
-                    fit="contain"
-                    :preview-src-list="data.file_list.map((x) => x.file_url)"
-                  />
-                </el-carousel-item>
-              </el-carousel>
-              <div class="container-box">
-                <button v-if="viewLeftRightBtn" class="arrow left" @click="scroll(-1)">
-                  <i class="el-icon-arrow-left"></i>
-                </button>
-                <ul ref="container" class="view-list-bottom" :style="{ width: elementWidth + 'px' }">
-                  <li v-for="(file, i) in data.file_list" :key="i" @click="handleIndicatorClick(i)">
-                    <el-image :id="file.file_id" :src="file.file_url" fit="contain" />
-                  </li>
-                </ul>
-                <button v-if="viewLeftRightBtn" class="arrow right" @click="scroll(1)">
-                  <i class="el-icon-arrow-right"></i>
-                </button>
-              </div>
-            </template>
-            <ul v-else class="view-independent">
-              <li v-for="(file, i) in data.file_list" :key="file.file_id" @click="handleIndicatorClick(i)">
+        <!-- 播放列表 -->
+        <div v-if="'list' === data.property.view_method">
+          <el-carousel
+            ref="pictureCarousel"
+            indicator-position="none"
+            :autoplay="false"
+            :style="{ height: elementHeight - 144 - 32 + 'px' }"
+            @change="handleChange"
+          >
+            <el-carousel-item v-for="(file, i) in data.file_list" :key="i">
+              <el-image
+                :id="file.file_id"
+                :src="file.file_url"
+                fit="contain"
+                :preview-src-list="data.file_list.map((x) => x.file_url)"
+              />
+            </el-carousel-item>
+          </el-carousel>
+          <div class="container-box">
+            <button v-if="viewLeftRightBtn" class="arrow left" @click="scroll(-1)">
+              <i class="el-icon-arrow-left"></i>
+            </button>
+            <ul ref="container" class="view-list-bottom" :style="{ width: elementWidth + 'px' }">
+              <li v-for="(file, i) in data.file_list" :key="i" @click="handleIndicatorClick(i)">
                 <el-image :id="file.file_id" :src="file.file_url" fit="contain" />
               </li>
             </ul>
+            <button v-if="viewLeftRightBtn" class="arrow right" @click="scroll(1)">
+              <i class="el-icon-arrow-right"></i>
+            </button>
           </div>
-        </template>
-        <!-- 独立排放 -->
-        <template v-else-if="data.property.view_method === 'independent'">
-          <div v-for="file in data.file_list" :key="file.file_id" class="alone-pic-area">
-            <el-image
-              :id="file.file_id"
-              :src="file.file_url"
-              fit="contain"
-              :preview-src-list="data.file_list.map((x) => x.file_url)"
-              :style="{ height: elementHeight + 'px' }"
-            />
-          </div>
-        </template>
+        </div>
+        <div
+          v-else
+          v-for="file in data.file_list"
+          :key="file.file_id"
+          class="alone-pic-area"
+          :style="getVideoItemStyle()"
+        >
+          <el-image
+            :id="file.file_id"
+            :src="file.file_url"
+            fit="contain"
+            :preview-src-list="data.file_list.map((x) => x.file_url)"
+            :style="{ height: elementHeight + 'px' }"
+          />
+        </div>
+
         <div v-if="'list' === data.property.view_method && isEnable(data.property.view_memo)" class="memo-area">
           <div v-for="(file, i) in data.file_info_list" :key="i">
             <div v-if="curPictureMemoIndex === i" class="title-div">{{ file.title ?? file.title }}</div>
@@ -88,17 +83,14 @@ export default {
       curPictureMemoIndex: 0,
       isResizing: false,
       resizeObserver: null,
-      isMore: false,
     };
   },
   watch: {
     data: {
       handler(val) {
         this.fileLen = val.file_list.length;
-        if (this.fileLen > 1) {
-          this.isMore = true;
-        }
-        if ((this.fileLen > 0 && this.data.property.view_method === 'list') || this.fileLen === 1) {
+        if (this.fileLen == 0) return;
+        if (this.data.property.view_method === 'list') {
           const ele = this.$refs.pictureAreaBox;
           const sn_position = this.data.property.sn_position;
           const viewMemo = this.isEnable(this.data.property.view_memo);
@@ -111,9 +103,9 @@ export default {
             this.elementHeight = ele.clientHeight;
           }
           // 统一图片播放模式位置调整和预览初始高度
-          if (ele.clientHeight <= 0) {
-            this.elementHeight = this.data.min_height - 60;
-          }
+          // if (ele.clientHeight <= 0) {
+          //    this.elementHeight = this.data.min_height - 60;
+          // }
 
           const mainEle = this.$refs.pictureArea;
           // 检查元素是否包含已知的类名
@@ -125,12 +117,35 @@ export default {
             }
           });
         }
+
+        if (this.data.property.view_method === 'independent') {
+          const ele = this.$refs.pictureAreaBox;
+          const sn_position = this.data.property.sn_position;
+          // 序号在左和右补齐序号高度,去掉padding(8*2)
+          if (sn_position.includes('left') || sn_position.includes('right')) {
+            this.elementWidth = ele.clientWidth - 16;
+            this.elementHeight = ele.clientHeight;
+          } else {
+            this.elementWidth = ele.clientWidth;
+            this.elementHeight = ele.clientHeight;
+          }
+          // 统一图片播放模式位置调整和预览初始高度
+          // if (ele.clientHeight <= 0) {
+          //    this.elementHeight = this.data.min_height - 60;
+          // }
+        }
+
+        this.elementHeight = Math.max(this.elementHeight, this.data.min_height);
+        this.$emit('handleHeightChange', this.id, this.elementHeight + 'px');
       },
       deep: true,
     },
     elementWidth() {
       this.isViewLeftRightBtn();
     },
+    fileLen() {
+      this.isViewLeftRightBtn();
+    },
   },
   mounted() {
     this.$nextTick(() => {
@@ -146,16 +161,12 @@ export default {
         this.elementWidth = viewMemo ? ele.clientWidth * 0.8 : ele.clientWidth;
         this.elementHeight = ele.clientHeight;
       }
-
       this.fileLen = this.data.file_list.length;
-      if (this.fileLen > 1) {
-        this.isMore = true;
-      }
-      this.isViewLeftRightBtn();
 
       this.resizeObserver = new ResizeObserver((entries) => {
         if (!this.getDragStatus()) return;
         this.isResizing = true; // 标记为调整中
+
         for (let entry of entries) {
           window.requestAnimationFrame(() => {
             const sn_position = this.data.property.sn_position;
@@ -172,8 +183,10 @@ export default {
             } else {
               this.elementWidth = w;
             }
+            this.elementHeight = Math.max(this.elementHeight, this.data.min_height);
           });
         }
+
         // 防抖:100ms 后恢复监听
         setTimeout(() => {
           this.isResizing = false;
@@ -188,6 +201,18 @@ export default {
     }
   },
   methods: {
+    getVideoItemStyle() {
+      if (this.data.property.view_method !== 'independent') {
+        return {};
+      }
+      const height = this.elementHeight > 0 ? this.elementHeight : Number(this.data.min_height);
+      const width = (height * 16) / 9;
+      return {
+        width: `${width}px`,
+        height: `${height}px`,
+        flexShrink: 0,
+      };
+    },
     // 是否显示左右箭头
     isViewLeftRightBtn() {
       // 计算底部列表图片宽度
@@ -240,6 +265,48 @@ export default {
 .picture-preview {
   @include preview-base;
 
+  .main {
+    position: relative;
+    overflow: auto hidden !important;
+
+    // Firefox 滚动条样式
+    scrollbar-width: none; // 默认隐藏
+
+    // 自定义滚动条样式 - 完全浮动
+    &::-webkit-scrollbar {
+      width: 0; // 默认宽度为 0
+      height: 0;
+      transition:
+        width 0.3s ease,
+        height 0.3s ease;
+    }
+
+    // 鼠标悬停时显示滚动条
+    &:hover::-webkit-scrollbar {
+      width: 8px;
+      height: 8px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background-color: rgba(0, 0, 0, 50%);
+      border-radius: 4px;
+      transition: background-color 0.3s ease;
+
+      &:hover {
+        background-color: rgba(0, 0, 0, 70%);
+      }
+    }
+
+    &::-webkit-scrollbar-track {
+      background-color: transparent;
+    }
+
+    &:hover {
+      scrollbar-width: thin; // 鼠标悬停时显示
+      scrollbar-color: rgba(0, 0, 0, 50%) transparent;
+    }
+  }
+
   .view-area {
     display: flex;
     column-gap: 7px;

+ 34 - 30
src/views/book/courseware/preview/components/video/VideoPreview.vue

@@ -23,7 +23,7 @@
           direction="vertical"
           :autoplay="false"
           :interval="0"
-          :style="{ width: elementWidth - 248 - 32 + 'px', height: elementHeight <= 0 ? 139 : elementHeight + 'px' }"
+          :style="{ width: elementWidth - 248 - 32 + 'px', height: elementHeight <= 0 ? 169 : elementHeight + 'px' }"
         >
           <el-carousel-item v-for="(file, i) in data.file_list" :key="i">
             <VideoPlay
@@ -34,7 +34,7 @@
             />
           </el-carousel-item>
         </el-carousel>
-        <div class="container-box" :style="{ height: elementHeight <= 0 ? 139 : elementHeight + 'px' }">
+        <div class="container-box" :style="{ height: elementHeight <= 0 ? 169 : elementHeight + 'px' }">
           <ul
             ref="container"
             class="view-list-bottom"
@@ -85,8 +85,8 @@ export default {
     data: {
       handler(val) {
         this.fileLen = val.file_list.length;
-
-        if (this.fileLen > 0 && this.data.property.view_method === 'list') {
+        if (this.fileLen == 0) return;
+        if (this.data.property.view_method === 'list') {
           const ele = this.$refs.videoAreaBox;
           const sn_position = this.data.property.sn_position;
           // 序号在左和右补齐序号高度,去掉padding(8*2)
@@ -98,9 +98,9 @@ export default {
             this.elementHeight = ele.clientHeight;
           }
 
-          if (ele.clientHeight <= 0) {
-            this.elementHeight = this.data.min_height;
-          }
+          // if (ele.clientHeight <= 0) {
+          //   this.elementHeight = this.data.min_height;
+          // }
 
           const mainEle = this.$refs.videoArea;
           // 检查元素是否包含已知的类名
@@ -113,7 +113,7 @@ export default {
           });
         }
 
-        if (this.fileLen > 0 && this.data.property.view_method === 'independent') {
+        if (this.data.property.view_method === 'independent') {
           const ele = this.$refs.videoAreaBox;
           const sn_position = this.data.property.sn_position;
           if (sn_position.includes('left') || sn_position.includes('right')) {
@@ -124,40 +124,44 @@ export default {
             this.elementHeight = ele.clientHeight;
           }
 
-          if (ele.clientHeight <= 0) {
-            this.elementHeight = this.data.min_height;
-          }
+          // if (ele.clientHeight <= 0) {
+          //   this.elementHeight = this.data.min_height;
+          // }
         }
+
+        this.elementHeight = Math.max(this.elementHeight, this.data.min_height);
+        // this.$emit('handleHeightChange', this.id, this.elementHeight + 'px');
       },
       deep: true,
     },
     elementHeight() {
       this.isViewTopBottomBtn();
     },
+    fileLen() {
+      this.isViewTopBottomBtn();
+    },
   },
   mounted() {
     this.$nextTick(() => {
-      const canvasElement = document.querySelector('.canvas');
-      if (!canvasElement) {
-        const ele = this.$refs.videoAreaBox;
-        const sn_position = this.data.property.sn_position;
-        // 序号在左和右补齐序号高度,去掉padding(8*2)
-        if (sn_position.includes('left') || sn_position.includes('right')) {
-          this.elementWidth = ele.clientWidth - 16;
-          this.elementHeight = ele.clientHeight + 30;
-        } else {
-          this.elementWidth = ele.clientWidth;
-          this.elementHeight = ele.clientHeight;
-        }
-
-        if (ele.clientHeight <= 0) {
-          this.elementHeight = this.data.min_height;
-        }
-        this.fileLen = this.data.file_list.length;
+      // const canvasElement = document.querySelector('.canvas');
+      // if (!canvasElement) {
+      const ele = this.$refs.videoAreaBox;
+      const sn_position = this.data.property.sn_position;
+      // 序号在左和右补齐序号高度,去掉padding(8*2)
+      if (sn_position.includes('left') || sn_position.includes('right')) {
+        this.elementWidth = ele.clientWidth - 16;
+        this.elementHeight = ele.clientHeight + 30;
+      } else {
+        this.elementWidth = ele.clientWidth;
+        this.elementHeight = ele.clientHeight;
+      }
 
-        this.isViewTopBottomBtn();
-        return;
+      if (ele.clientHeight <= 0) {
+        this.elementHeight = this.data.min_height;
       }
+      this.fileLen = this.data.file_list.length;
+
+      //}
 
       this.resizeObserver = new ResizeObserver((entries) => {
         if (!this.getDragStatus() && !this.isFirstLoad) return;