Procházet zdrojové kódy

1.0.2 富文本编辑器图片增加尺寸调整框

liuyanpeng před 10 měsíci
rodič
revize
53034e93a9
2 změnil soubory, kde provedl 532 přidání a 11 odebrání
  1. 1 1
      package.json
  2. 531 10
      src/widget/EditorWidget.vue

+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
   "name": "client-trace-v5",
   "description": "client-trace-v5",
-  "version": "1.0.1",
+  "version": "1.0.2",
   "author": "yangzhijie <yangzhijie1488@163.com>",
   "scripts": {
     "dev": "webpack serve --config ./webpack.dev.js",

+ 531 - 10
src/widget/EditorWidget.vue

@@ -1,8 +1,46 @@
 <template>
-  <div id="editor" />
-  <!-- eslint-disable-next-line -->
-  <!-- <div id="content-editor-view" class="view-box" v-html="modelValue"/> -->
-  <Loading v-if="loading" />
+  <div class="editor-container">
+    <div id="editor" />
+    <!-- eslint-disable-next-line -->
+    <!-- <div id="content-editor-view" class="view-box" v-html="modelValue"/> -->
+    <Loading v-if="loading" />
+
+    <!-- 图片调整模态框 -->
+    <div
+      v-if="showImageModal" class="image-resize-modal" :style="{
+        top: modalPosition.top + 'px',
+        left: modalPosition.left + 'px'
+      }"
+    >
+      <div class="modal-content">
+        <!-- 一行显示:缩放控制、宽度、高度 -->
+        <div class="controls-row">
+          <div class="zoom-controls">
+            <button :disabled="currentZoom <= minZoom" class="zoom-btn" title="缩小 (10%)" @click="zoomOut">
+              <span>-</span>
+            </button>
+            <span class="zoom-display">{{ currentZoom && Math.round(currentZoom * 100) }}%</span>
+            <button :disabled="currentZoom >= maxZoom" class="zoom-btn" title="放大 (10%)" @click="zoomIn">
+              <span>+</span>
+            </button>
+          </div>
+
+          <div class="size-input-group">
+            <label>宽度:</label>
+            <input v-model="imageSize.width" type="number" @input="updateImageSize" @keydown.enter.prevent />
+            <span>px</span>
+          </div>
+
+          <div class="size-input-group">
+            <label>高度:</label>
+            <input v-model="imageSize.height" type="number" @input="updateImageSize" @keydown.enter.prevent />
+            <span>px</span>
+          </div>
+        </div>
+        <button class="close-btn" title="关闭" @click="closeImageModal">×</button>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script>
@@ -24,10 +62,6 @@ export default {
       type: String,
       default: '',
     },
-    traceId: {
-      type: String,
-      default: null,
-    },
   },
   emits: ['update:modelValue'],
 
@@ -35,6 +69,16 @@ export default {
     return {
       loading: false,
       traceConfigDto: {},
+      // 图片调整模态框相关数据
+      showImageModal: false,
+      currentImage: null,
+      originalImageSize: { width: 0, height: 0 },
+      imageSize: { width: 0, height: 0 },
+      modalPosition: { top: 0, left: 0 },
+      currentZoom: 1,
+      minZoom: 0.1, // 最小10%
+      maxZoom: 5.0, // 最大500%
+      contentChangeTimer: null, // 添加防抖定时器
     };
   },
 
@@ -185,6 +229,8 @@ export default {
               const html = $('#editor').trumbowyg('html');
               const newHtml = html.replace(`[图片上传中${tempId}]`, `<img src="${imagUrl}" alt="图片" />`);
               $('#editor').trumbowyg('html', newHtml);
+              // 将光标定位到图片后面
+              _self.setCursorPosition(imagUrl);
             });
           }
         }
@@ -211,12 +257,42 @@ export default {
     $('#editor').on('tbwchange', () => {
       const html = $('#editor').trumbowyg('html');
       this.$emit('update:modelValue', html);
+
+      // 检查当前选中的图片是否还存在,如果不存在则关闭模态框
+      this.checkImageExistence();
+    });
+
+    // 通过 Trumbowyg 事件系统监听编辑器变化
+    $('#editor').on('tbwchange tbwinit', () => {
+      // 每次内容变化后重新绑定图片点击事件
+      setTimeout(() => {
+        this.bindImageClickEvents();
+      }, 100);
+    });
+
+    // 点击其他地方关闭模态框,同时作为图片点击的备用监听器
+    $(document).on('click', e => {
+      // 如果点击的是编辑器内的图片,显示调整模态框
+      if (e.target.tagName === 'IMG' && $(e.target).closest('#editor').length) {
+        e.preventDefault();
+        e.stopPropagation();
+        this.showImageResizeModal(e.target);
+        return;
+      }
+
+      // 如果点击的不是模态框或图片,关闭模态框
+      if (!$(e.target).closest('.image-resize-modal, .trumbowyg-editor img, #editor img').length) {
+        this.closeImageModal();
+      }
     });
 
     // Common.downloadByA();
     this.getTraceConfig();
   },
   unmounted() {
+    if (this.contentChangeTimer) {
+      clearTimeout(this.contentChangeTimer);
+    }
     $('#editor').trumbowyg('destroy');
   },
   methods: {
@@ -228,7 +304,6 @@ export default {
 
     // 上传图片
     async uploadImg(file, insertFn) {
-      console.log(file);
       const _self = this;
       const formData = new FormData();
       try {
@@ -329,13 +404,340 @@ export default {
         },
       });
     },
-  },
+    // 获取图片缩放配置
+    showImageResizeModal(imgElement) {
+      this.currentImage = imgElement;
+
+      // 获取图片设置的样式尺寸(用户实际设置的值,不受容器限制影响)
+      const setWidth = parseInt(imgElement.style.width) || imgElement.naturalWidth;
+      const setHeight = parseInt(imgElement.style.height) || imgElement.naturalHeight;
+
+      // 如果没有设置样式,使用计算样式作为备用
+      const computedStyle = window.getComputedStyle(imgElement);
+      const currentWidth = setWidth || parseInt(computedStyle.width) || imgElement.naturalWidth;
+      const currentHeight = setHeight || parseInt(computedStyle.height) || imgElement.naturalHeight;
+
+      // 保存原始尺寸(如果是第一次点击)
+      if (!imgElement.dataset.originalWidth) {
+        imgElement.dataset.originalWidth = imgElement.naturalWidth;
+        imgElement.dataset.originalHeight = imgElement.naturalHeight;
+      }
+
+      this.originalImageSize = {
+        width: parseInt(imgElement.dataset.originalWidth),
+        height: parseInt(imgElement.dataset.originalHeight),
+      };
+
+      this.imageSize = {
+        width: currentWidth,
+        height: currentHeight,
+      };
+
+      // 计算当前缩放比例
+      if (this.originalImageSize.width == 0) {
+        this.currentZoom = 1;
+      } else {
+        this.currentZoom = currentWidth / this.originalImageSize.width;
+      }
+
+      // 计算模态框位置
+      this.calculateModalPosition(imgElement);
+
+      this.showImageModal = true;
+    },
+
+    // 计算模态框位置的独立方法
+    calculateModalPosition(imgElement) {
+      const imgRect = imgElement.getBoundingClientRect();
+      const editorContainer = document.querySelector('.editor-container');
+      const containerRect = editorContainer.getBoundingClientRect();
+
+      // 获取Trumbowyg编辑器的box容器(包含工具栏和编辑区域)
+      const trumbowygBox = document.querySelector('.trumbowyg-box');
+      const trumbowygBoxRect = trumbowygBox ? trumbowygBox.getBoundingClientRect() : null;
+
+      // 模态框宽度
+      const modalWidth = 420;
+
+      // 编辑器固定高度
+      const editorHeight = 368;
+
+      let top, left;
+
+      // 判断是否需要将模态框显示在编辑器下方
+      const shouldShowBelowEditor = this.shouldShowModalBelowEditor(imgRect, trumbowygBoxRect, editorHeight);
+
+      if (shouldShowBelowEditor) {
+        // 模态框显示在编辑器容器的正下方
+        if (trumbowygBoxRect) {
+          top = trumbowygBoxRect.bottom - containerRect.top + 5;
+        } else {
+          // 备用方案:使用固定位置
+          top = editorHeight + 40; // 工具栏高度约40px
+        }
+
+        // 水平位置:居中显示在编辑器下方
+        const centerX = trumbowygBoxRect ? (trumbowygBoxRect.left + trumbowygBoxRect.right) / 2 : containerRect.left + containerRect.width / 2;
+        left = centerX - containerRect.left - modalWidth * 2;
+
+        // 确保不超出容器边界
+        left = Math.max(0, Math.min(left, containerRect.width - modalWidth));
+      } else {
+        // 小图片且在可视区域内:模态框显示在图片正下方
+        top = imgRect.bottom - containerRect.top + 5;
+        left = imgRect.left - containerRect.left;
+
+        // 确保不超出容器边界
+        left = Math.max(0, Math.min(left, containerRect.width - modalWidth));
+      }
+
+      this.modalPosition = { top, left };
+    },
+
+    // 判断是否应该将模态框显示在编辑器下方
+    shouldShowModalBelowEditor(imgRect, trumbowygBoxRect, editorHeight) {
+      // 情况1:图片本身高度超过编辑器高度
+      if (this.imageSize.height > editorHeight) {
+        return true;
+      }
+
+      // 情况2:图片底部超出了编辑器的可视区域
+      if (trumbowygBoxRect) {
+        // 获取编辑器内容区域的底部位置(不包括工具栏)
+        const editorContentBottom = trumbowygBoxRect.bottom;
+
+        // 如果图片底部超出了编辑器内容区域,说明图片有部分在滚动区域下方
+        if (imgRect.bottom > editorContentBottom) {
+          return true;
+        }
+      }
+
+      // 情况3:图片顶部在编辑器可视区域上方(图片被向上滚动了)
+      if (trumbowygBoxRect) {
+        // 获取编辑器内容区域的顶部位置(工具栏下方)
+        const trumbowyg = $('#editor').data('trumbowyg');
+        const toolbarHeight = trumbowyg && trumbowyg.$btnPane ? trumbowyg.$btnPane.height() : 40;
+        const editorContentTop = trumbowygBoxRect.top + toolbarHeight;
+
+        // 如果图片顶部在编辑器内容区域上方,说明图片被部分滚动到上方了
+        if (imgRect.top < editorContentTop) {
+          return true;
+        }
+      }
+
+      return false;
+    },
+
+    // 关闭图片模态框
+    closeImageModal() {
+      this.showImageModal = false;
+      this.currentImage = null;
+    },
+
+    // 更新图片尺寸
+    updateImageSize() {
+      if (!this.currentImage) return;
+
+      const width = parseInt(this.imageSize.width);
+      const height = parseInt(this.imageSize.height);
+
+      if (width > 0 && height > 0) {
+        this.currentImage.style.width = width + 'px';
+        this.currentImage.style.height = height + 'px';
+
+        // 更新缩放比例
+        this.currentZoom = width / this.originalImageSize.width;
+
+        // 强制同步编辑器内容
+        this.syncEditorContent();
+      }
+    },
+
+    // 放大
+    zoomIn() {
+      if (this.currentZoom >= this.maxZoom) return;
+
+      this.currentZoom = Math.min(this.currentZoom + 0.1, this.maxZoom);
+      this.applyZoom();
+    },
+
+    // 缩小
+    zoomOut() {
+      if (this.currentZoom <= this.minZoom) return;
+
+      this.currentZoom = Math.max(this.currentZoom - 0.1, this.minZoom);
+      this.applyZoom();
+    },
+
+    // 触发缩放事件
+    applyZoom() {
+      if (!this.currentImage) return;
+
+      const newWidth = Math.round(this.originalImageSize.width * this.currentZoom);
+      const newHeight = Math.round(this.originalImageSize.height * this.currentZoom);
+
+      // 直接设置模态框显示的尺寸(用户设置的目标尺寸)
+      this.imageSize.width = newWidth;
+      this.imageSize.height = newHeight;
+
+      // 设置图片的样式尺寸
+      this.currentImage.style.width = newWidth + 'px';
+      this.currentImage.style.height = newHeight + 'px';
+
+      // 强制同步编辑器内容
+      this.syncEditorContent();
+    },
+
+    // 更新模态框中显示的尺寸值(获取图片设置的样式尺寸)
+    updateDisplayedSize() {
+      if (!this.currentImage) return;
+
+      // 获取图片设置的样式尺寸(用户实际设置的值)
+      const setWidth = parseInt(this.currentImage.style.width) || this.currentImage.naturalWidth;
+      const setHeight = parseInt(this.currentImage.style.height) || this.currentImage.naturalHeight;
+
+      // 更新模态框中显示的尺寸值(显示用户设置的值,不是被限制后的显示值)
+      this.imageSize = {
+        width: setWidth,
+        height: setHeight,
+      };
+
+      // 更新缩放比例(基于用户设置的尺寸)
+      if (this.originalImageSize.width > 0) {
+        this.currentZoom = setWidth / this.originalImageSize.width;
+      }
+    },
+    // 同步编辑器内容
+    syncEditorContent() {
+      const trumbowyg = $('#editor').data('trumbowyg');
+      if (trumbowyg) {
+        // 强制同步DOM到HTML
+        trumbowyg.syncCode();
+
+        // 延迟触发内容变化事件
+        this.debouncedTriggerContentChange();
+      }
+    },
+    // 防抖的内容变化触发器
+    debouncedTriggerContentChange() {
+      if (this.contentChangeTimer) {
+        clearTimeout(this.contentChangeTimer);
+      }
+      this.contentChangeTimer = setTimeout(() => {
+        this.triggerContentChange();
+      }, 300); // 300ms 防抖
+    },
+    // 触发编辑器内容变化事件
+    triggerContentChange() {
+      const html = $('#editor').trumbowyg('html');
+      this.$emit('update:modelValue', html);
+    },
+
+
+    // 给所有图片绑定点击事件
+    bindImageClickEvents() {
+      const _self = this;
+      const trumbowyg = $('#editor').data('trumbowyg');
+
+      if (!trumbowyg || !trumbowyg.$ed) {
+        console.log('bindImageClickEvents: Trumbowyg 编辑器未初始化');
+        return;
+      }
+
+      // 查找所有图片
+      const images = trumbowyg.$ed.find('img');
+
+      // 先移除所有已有的图片点击事件,避免重复绑定
+      images.off('click.imageResize');
+
+      // 为每个图片添加点击事件
+      images.each(function () {
+        const img = $(this);
+
+        img.on('click.imageResize', function (e) {
+          e.preventDefault();
+          e.stopPropagation();
+          _self.showImageResizeModal(this);
+        });
+      });
+    },
+
+    // 粘贴后光标位于图片后
+    setCursorPosition(imagUrl) {
+      setTimeout(() => {
+        try {
+          const $editor = $('#editor');
+          const editorElement = $editor.data('trumbowyg').$ed[0];
+
+          // 找到刚插入的图片元素
+          const imgElements = editorElement.querySelectorAll(`img[src="${imagUrl}"]`);
+          const imgElement = imgElements[imgElements.length - 1]; // 获取最后一个(最新插入的)
+
+          if (imgElement) {
+            const range = document.createRange();
+            const selection = window.getSelection();
+
+            // 直接将光标定位在图片元素后面
+            range.setStartAfter(imgElement);
+            range.setEndAfter(imgElement);
+
+            selection.removeAllRanges();
+            selection.addRange(range);
+
+            // 确保编辑器获得焦点
+            editorElement.focus();
+          }
+        } catch (error) {
+          console.error('设置光标位置失败:', error);
+        }
+      }, 100); // 延迟确保DOM更新完成
+    },
+
+    // 检查当前选中的图片是否还存在于编辑器中
+    checkImageExistence() {
+      // 如果没有显示模态框或没有当前图片,直接返回
+      if (!this.showImageModal || !this.currentImage) {
+        return;
+      }
+
+      try {
+        const trumbowyg = $('#editor').data('trumbowyg');
+        if (!trumbowyg || !trumbowyg.$ed) {
+          return;
+        }
 
+        // 获取编辑器中所有的图片元素
+        const editorImages = trumbowyg.$ed.find('img');
 
+        // 检查当前选中的图片是否还在编辑器中
+        let imageExists = false;
+        editorImages.each((_, img) => {
+          if (img === this.currentImage) {
+            imageExists = true;
+            return false; // 跳出each循环
+          }
+        });
+
+        // 如果图片不存在了,关闭模态框
+        if (!imageExists) {
+          console.log('检测到图片已被删除,关闭模态框');
+          this.closeImageModal();
+        }
+      } catch (error) {
+        console.error('检查图片存在性时出错:', error);
+        // 出错时也关闭模态框,避免显示无效的模态框
+        this.closeImageModal();
+      }
+    },
+  },
 };
 </script>
 
 <style>
+.editor-container {
+  position: relative;
+}
+
 .trumbowyg-box .trumbowyg-editor {
   height: 368px !important;
 }
@@ -354,4 +756,123 @@ export default {
   min-height: 200px;
   padding: 8px;
 }
+
+/* 图片调整模态框样式 */
+.image-resize-modal {
+  position: absolute;
+  z-index: 1000;
+  background: white;
+  border: 1px solid #ddd;
+  border-radius: 6px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  padding: 0;
+  min-width: 420px;
+  max-width: 500px;
+}
+
+.modal-content {
+  position: relative;
+  padding: 4px 12px;
+}
+
+.controls-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin: 12px;
+  justify-content: space-between;
+}
+
+.size-input-group {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.size-input-group label {
+  font-size: 12px;
+  color: #666;
+  min-width: 32px;
+}
+
+.size-input-group input {
+  width: 60px;
+  padding: 4px 6px;
+  border: 1px solid #ddd;
+  border-radius: 3px;
+  font-size: 12px;
+  text-align: center;
+}
+
+.size-input-group span {
+  font-size: 12px;
+  color: #999;
+}
+
+.zoom-controls {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.zoom-btn {
+  width: 28px;
+  height: 28px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  background: white;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  font-weight: bold;
+  transition: all 0.2s;
+}
+
+.zoom-btn:hover:not(:disabled) {
+  background: #f5f5f5;
+  border-color: #999;
+}
+
+.zoom-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.zoom-display {
+  font-size: 12px;
+  color: #666;
+  min-width: 40px;
+  text-align: center;
+}
+
+.close-btn {
+  position: absolute;
+  top: 4px;
+  right: 4px;
+  width: 20px;
+  height: 20px;
+  border: none;
+  background: none;
+  cursor: pointer;
+  font-size: 24px;
+  color: #999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 2px;
+}
+
+.close-btn:hover {
+  background: #f5f5f5;
+  color: #666;
+}
+
+/* 图片选中状态 */
+.trumbowyg-editor img:hover {
+  outline: 2px solid #007bff;
+  outline-offset: 2px;
+  cursor: pointer;
+}
 </style>