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

feat: 添加电子印章管理及文档盖章功能

yusheng 2 месяцев назад
Родитель
Сommit
1813f4d2ff

+ 2 - 0
package.json

@@ -41,8 +41,10 @@
     "element-ui": "2.15.7",
     "github-markdown-css": "^5.1.0",
     "highlight.js": "9.18.5",
+    "html2canvas": "^1.4.1",
     "jsbarcode": "^3.11.5",
     "json-bigint": "^1.0.0",
+    "jspdf": "^2.5.2",
     "lodash": "^4.17.21",
     "mathjs": "^12.4.1",
     "nprogress": "^0.2.0",

+ 12 - 0
src/api/bpm/components/doc/index.js

@@ -31,3 +31,15 @@ import request from '@/utils/request';
   }
   return Promise.reject(new Error(res.data.message));
 }
+/**
+ * 修改
+ * @data data
+ */
+export async function fileUpdateAPI(data) {
+  data.authority = data.authority || 0
+  const res = await request.put('/fm/file/update', data);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}

+ 162 - 0
src/api/bpm/components/sealManagement/index.js

@@ -0,0 +1,162 @@
+import request from '@/utils/request';
+
+/**
+ * 新增印章
+ * @param {Object} data 印章数据
+ * @returns {Promise}
+ */
+export async function addSeal(data) {
+  const res = await request.post('/seal/management/add', data);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 变更印章
+ * @param {Object} data 印章数据
+ * @returns {Promise}
+ */
+export async function changeSeal(data) {
+  const res = await request.post('/seal/management/change', data);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 删除印章
+ * @param {string|number} id 印章ID
+ * @returns {Promise}
+ */
+export async function deleteSeal(id) {
+  const res = await request.get(`/seal/management/delete/${id}`);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 停用印章
+ * @param {string|number} id 印章ID
+ * @returns {Promise}
+ */
+export async function disableSeal(id) {
+  const res = await request.put(`/seal/management/disable/${id}`);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 启用印章
+ * @param {string|number} id 印章ID
+ * @returns {Promise}
+ */
+export async function enableSeal(id) {
+  const res = await request.put(`/seal/management/enable/${id}`);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 根据ID查询印章详情
+ * @param {string|number} id 印章ID
+ * @returns {Promise}
+ */
+export async function getSealById(id) {
+  const res = await request.get(`/seal/management/getById/${id}`);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 查询印章历史版本
+ * @param {string|number} sealId 印章ID
+ * @returns {Promise}
+ */
+export async function getSealHistoryList(sealId) {
+  const res = await request.get(`/seal/management/getHistoryList/${sealId}`);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 分页查询印章列表
+ * @param {Object} params 查询参数
+ * @returns {Promise}
+ */
+export async function getSealPage(data) {
+  const res = await request.post(`/seal/management/page`, data);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 分页查询公用章列表
+ * @param {Object} data 查询参数
+ * @returns {Promise}
+ */
+export async function getPublicSealPage(data) {
+  const res = await request.post(`/seal/management/public-page`, data);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 分页查询个人章列表
+ * @param {Object} data 查询参数
+ * @returns {Promise}
+ */
+export async function getPrivateSealPage(data) {
+  const res = await request.post(`/seal/management/private-page`, data);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 更新印章
+ * @param {Object} data 印章数据
+ * @returns {Promise}
+ */
+export async function updateSeal(data) {
+  const res = await request.post('/seal/management/update', data);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/**
+ * 根据印章ID查询使用记录列表
+ * @param {string|number} id 印章ID
+ * @param {Object} params 查询参数
+ * @returns {Promise}
+ */
+export async function getSealUsageRecordList(id, params) {
+  let par = new URLSearchParams(params);
+  const res = await request.get(
+    `/seal/management/usageRecord/listBySealId/${id}?` + par,
+    {}
+  );
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}

BIN
src/assets/0.jpg


BIN
src/assets/1.jpg


BIN
src/assets/2.jpg


BIN
src/assets/3.jpg


BIN
src/assets/4.jpg


BIN
src/assets/5.jpg


+ 1372 - 0
src/components/addDoc/seal.vue

@@ -0,0 +1,1372 @@
+<template>
+  <div class="app-container">
+    <div class="tool-card">
+      <div class="converter-layout">
+        <!-- 右侧: 预览 & Canvas 结果 -->
+        <div id="captureTarget" style="position: fixed; left: -100000px">
+          <div v-html="editableHtml"> </div>
+        </div>
+        <div class="preview-panel">
+          <div class="canvas-result">
+            <canvas id="outputCanvas" v-show="canvasGenerated"></canvas>
+            <!-- <div class="canvas-buttons" v-if="canvasGenerated">
+              <button @click="downloadCanvasAsImage" class="download">
+                📥 下载图片
+              </button>
+              <button @click="exportToPDF" class="pdf"> 📄 导出PDF </button>
+              <button @click="printCanvas" class="print"> 🖨️ 打印 </button>
+            </div> -->
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- 图片素材区域 -->
+    <div class="images-panel">
+      <!-- 公用章 -->
+      <div class="seal-section">
+        <div class="panel-title">公用章</div>
+        <div class="images-grid">
+          <div
+            v-for="(img, index) in publicImages"
+            :key="'public-' + index"
+            class="drag-item"
+            draggable="true"
+            @dragstart="handleDragStart($event, img, index, 'public')"
+          >
+            <img :src="img.imgUrl" :alt="img.name" />
+            <div class="drag-item-name">{{ img.name }}</div>
+          </div>
+          <div v-if="!publicImages.length" class="no-data">暂无公用章</div>
+        </div>
+      </div>
+      <!-- 个人章 -->
+      <div class="seal-section">
+        <div class="panel-title">个人章</div>
+        <div class="images-grid">
+          <div
+            v-for="(img, index) in privateImages"
+            :key="'private-' + index"
+            class="drag-item"
+            draggable="true"
+            @dragstart="handleDragStart($event, img, index, 'private')"
+          >
+            <img :src="img.imgUrl" :alt="img.name" />
+            <div class="drag-item-name">{{ img.name }}</div>
+          </div>
+          <div v-if="!privateImages.length" class="no-data">暂无个人章</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+  import {
+    getSealPage,
+    getPrivateSealPage
+  } from '@/api/bpm/components/sealManagement';
+  import { getUserDetail } from '@/api/system/organization/index';
+  // 引入 html2canvas 和 jsPDF
+  import html2canvas from 'html2canvas';
+  import jsPDF from 'jspdf';
+//   const imageUrl = [
+//     require('@/assets/0.jpg'),
+//     require('@/assets/1.jpg'),
+//     require('@/assets/2.jpg'),
+//     require('@/assets/3.jpg'),
+//     require('@/assets/4.jpg'),
+//     require('@/assets/5.jpg')
+//   ]; // 使用相对路径或别名路径
+//   // 默认精美的示例HTML + CSS (内联样式丰富,展示能力)
+//   const DEFAULT_HTML = `
+//  <div class="container">
+//         <div class="img-area">
+//             <img class="my-photo" alt="loading" src="${imageUrl[0]}">
+//         </div>
+//         <div class="img-area">
+//             <img class="my-photo" alt="loading"  src="${imageUrl[1]}">
+//         </div>
+//              <div class="img-area">
+//             <img class="my-photo" alt="loading"  src="${imageUrl[2]}">
+//         </div>      <div class="img-area">
+//             <img class="my-photo" alt="loading"  src="${imageUrl[3]}">
+//         </div>      <div class="img-area">
+//             <img class="my-photo" alt="loading"  src="${imageUrl[4]}">
+//         </div>      <div class="img-area">
+//             <img class="my-photo" alt="loading"  src="${imageUrl[5]}">
+//         </div>
+// </div>
+// `;
+
+  export default {
+    name: 'HtmlToCanvas',
+    data() {
+      return {
+        editableHtml: '',
+        previewHtml: '',
+        canvasGenerated: false,
+        publicImages: [],
+        privateImages: [],
+        droppedImages: [],
+        selectedImage: null,
+        baseCanvasData: null // 缓存基础内容的 Canvas 数据
+      };
+    },
+    created() {
+      // 获取公用章列表
+      getSealPage({
+        page: 999,
+        limit: 1,
+        status: 1,
+        approvalStatus: 2,
+        sealHolderId:this.$store.state.user.info.userId
+      }).then((res) => {
+        this.publicImages = res.list;
+      });
+      // 获取个人章列表
+      getUserDetail(this.$store.state.user.info.userId).then((res) => {
+        if (res.signature?.[0]) {
+          this.privateImages = [
+            {
+              imgUrl: res.signature[0].url,
+              name: res.name
+            }
+          ];
+          console.log(this.privateImages, 'this.privateImages ');
+        }
+      });
+    },
+    mounted() {
+      // 组件挂载后自动渲染一次
+      this.$nextTick(() => {
+        this.renderToCanvas();
+      });
+
+      // 保存组件实例引用,用于事件处理器
+      window._aaComponentInstance = this;
+
+      // 创建全局安全移除函数
+      window._safeRemoveElement = function (element) {
+        try {
+          if (
+            element &&
+            element.parentNode &&
+            document.body.contains(element)
+          ) {
+            element.parentNode.removeChild(element);
+            return true;
+          }
+        } catch (err) {
+          console.warn('安全移除元素时发生警告:', err);
+        }
+        return false;
+      };
+    },
+    beforeDestroy() {
+      // 清理引用
+      if (window._aaComponentInstance === this) {
+        window._aaComponentInstance = null;
+      }
+      if (window._safeRemoveElement) {
+        window._safeRemoveElement = null;
+      }
+    },
+    methods: {
+      // 获取组件实例
+      getComponentInstance() {
+        return window._aaComponentInstance || this;
+      },
+
+      // 安全的移除DOM元素
+      safeRemoveElement(elementId) {
+        try {
+          const element = document.getElementById(elementId);
+          if (window._safeRemoveElement) {
+            return window._safeRemoveElement(element);
+          }
+          // 回退方案
+          if (
+            element &&
+            element.parentNode &&
+            document.body.contains(element)
+          ) {
+            element.parentNode.removeChild(element);
+            return true;
+          }
+          return false;
+        } catch (err) {
+          console.warn(`移除元素 ${elementId} 时发生警告:`, err);
+          return false;
+        }
+      },
+
+      // 安全移除所有菜单
+      safeRemoveAllMenus() {
+        try {
+          const existingMenus = document.querySelectorAll('[id^="menu_"]');
+
+          // 使用倒序循环,避免在移除时影响NodeList
+          for (let i = existingMenus.length - 1; i >= 0; i--) {
+            const menu = existingMenus[i];
+            if (menu && document.body.contains(menu) && menu.parentNode) {
+              try {
+                menu.parentNode.removeChild(menu);
+              } catch (err) {
+                console.warn('移除菜单时发生警告:', err);
+              }
+            }
+          }
+        } catch (err) {
+          console.warn('移除所有菜单时发生错误:', err);
+        }
+      },
+      // 设置 Canvas 拖放区域和点击事件
+      setupCanvasDropZone() {
+        const canvas = document.getElementById('outputCanvas');
+        if (!canvas) {
+          console.log('Canvas 元素不存在');
+          return;
+        }
+
+        console.log('设置拖放区域和点击事件,Canvas ID:', canvas.id);
+        console.log('Canvas 尺寸:', canvas.width, 'x', canvas.height);
+        console.log(
+          'Canvas 可见性:',
+          canvas.style.display,
+          canvas.style.visibility
+        );
+        console.log('Canvas 位置:', canvas.getBoundingClientRect());
+
+        canvas.addEventListener('dragover', (e) => {
+          e.preventDefault();
+          e.dataTransfer.dropEffect = 'copy';
+        });
+
+        canvas.addEventListener('drop', (e) => {
+          e.preventDefault();
+          console.log('触发 drop 事件');
+          const imageData = e.dataTransfer.getData('imageData');
+          console.log('获取到的 imageData:', imageData);
+          if (imageData) {
+            const data = JSON.parse(imageData);
+            console.log('解析后的数据:', data);
+            this.addImageToCanvas(data.url, e.offsetX, e.offsetY);
+          }
+        });
+
+        // 添加右键点击事件监听
+        canvas.addEventListener('contextmenu', (e) => {
+          e.preventDefault();
+          this.handleCanvasRightClick(e);
+        });
+      },
+
+      // 处理拖拽开始
+      handleDragStart(e, img, index) {
+        const data = {
+          url: img.imgUrl,
+          name: img.name
+        };
+        e.dataTransfer.setData('imageData', JSON.stringify(data));
+        e.dataTransfer.effectAllowed = 'copy';
+        console.log('拖拽开始:', data);
+      },
+
+      // 添加图片到 Canvas
+      addImageToCanvas(imgUrl, x, y) {
+        const canvas = document.getElementById('outputCanvas');
+        if (!canvas || !this.canvasGenerated) {
+          console.log('请先渲染 Canvas');
+
+          return;
+        }
+
+        console.log('Canvas 尺寸:', canvas.width, canvas.height);
+        console.log('拖放位置:', x, y);
+
+        const ctx = canvas.getContext('2d');
+        const img = new Image();
+        img.crossOrigin = 'anonymous';
+
+        img.onload = () => {
+          console.log('图片加载成功,原始尺寸:', img.width, img.height);
+
+          // 默认图片大小为 100x100
+          const width = 100;
+          const height = 100;
+          const posX = x - width / 2;
+          const posY = y - height / 2;
+
+          console.log('绘制位置:', posX, posY, '尺寸:', width, height);
+
+          // 绘制图片
+          ctx.drawImage(img, posX, posY, width, height);
+
+          const imageId = `img_${Date.now()}_${Math.random()
+            .toString(36)
+            .substr(2, 9)}`;
+
+          this.droppedImages.push({
+            id: imageId,
+            imgUrl: imgUrl,
+            x: posX,
+            y: posY,
+            width,
+            height,
+            originalWidth: img.width,
+            originalHeight: img.height
+          });
+
+          console.log('已添加的图片数量:', this.droppedImages.length);
+          this.$message({
+            showClose: true,
+            message: '操作成功',
+            type: 'success'
+          });
+
+          // 设置图片可再次拖动和调整(通过右键菜单)
+          // 不再创建覆盖层,仅通过右键菜单操作
+        };
+
+        img.onerror = (err) => {
+          console.error('图片加载失败:', err);
+          this.$message({
+            showClose: true,
+            message: '操作失败',
+            type: 'error'
+          });
+          // this.showMessage('⚠️ 图片加载失败', 'error');
+        };
+
+        img.src = imgUrl;
+      },
+
+      // 重新绘制整个 Canvas
+      redrawCanvas() {
+        const canvas = document.getElementById('outputCanvas');
+        if (!canvas || !this.canvasGenerated) return;
+
+        const ctx = canvas.getContext('2d');
+
+        try {
+          // 清空 Canvas
+          ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+          // 如果有缓存的基础内容,直接绘制
+          if (this.baseCanvasData) {
+            ctx.drawImage(this.baseCanvasData, 0, 0);
+          } else {
+            // 如果没有缓存,重新渲染基础内容
+            console.warn('没有缓存的基础内容,可能需要重新渲染');
+            return;
+          }
+
+          // 重新绘制所有拖入的图片
+          this.droppedImages.forEach((image) => {
+            const img = new Image();
+            img.onload = () => {
+              ctx.drawImage(img, image.x, image.y, image.width, image.height);
+            };
+            img.src = image.imgUrl;
+          });
+
+          console.log(
+            'Canvas 已快速重绘,包含基础内容和',
+            this.droppedImages.length,
+            '张图片'
+          );
+
+          // 确保事件监听仍然有效
+          setTimeout(() => {
+            this.setupCanvasDropZone();
+          }, 100);
+        } catch (error) {
+          console.error('重新绘制 Canvas 失败:', error);
+        }
+      },
+
+      // 显示图片控制菜单
+      showImageControls(imageId, x, y) {
+        console.log('显示图片控制菜单,位置:', x, y, '图片ID:', imageId);
+
+        // 保存组件实例引用
+        const self = this;
+
+        // 先移除可能存在的其他菜单
+        this.safeRemoveAllMenus();
+
+        // 定义菜单选项(使用局部变量 self)
+        const options = [
+          { text: '放大', action: () => self.resizeImage(imageId, 1.2) },
+          { text: '缩小', action: () => self.resizeImage(imageId, 0.8) },
+          { text: '移动', action: () => self.startMoveImage(imageId, x, y) },
+          { text: '删除', action: () => self.removeImage(imageId) }
+        ];
+
+        const menu = document.createElement('div');
+        menu.id = `menu_${imageId}`;
+        menu.style.position = 'fixed';
+
+        // 调整位置,避免菜单超出屏幕边界
+        const menuWidth = 120;
+        const menuHeight = options.length * 40 + 16; // 估算高度
+
+        let left = x;
+        let top = y;
+
+        // 如果菜单会超出右侧边界,向左移动
+        if (left + menuWidth > window.innerWidth) {
+          left = window.innerWidth - menuWidth - 10;
+        }
+
+        // 如果菜单会超出底部边界,向上移动
+        if (top + menuHeight > window.innerHeight) {
+          top = window.innerHeight - menuHeight - 10;
+        }
+
+        menu.style.left = `${left}px`;
+        menu.style.top = `${top}px`;
+        menu.style.background = 'white';
+        menu.style.border = '1px solid #e2e8f0';
+        menu.style.borderRadius = '8px';
+        menu.style.padding = '8px 0';
+        menu.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)';
+        menu.style.zIndex = '9999'; // 使用更高的 z-index
+        menu.style.minWidth = `${menuWidth}px`;
+        menu.style.opacity = '1';
+        menu.style.visibility = 'visible';
+        menu.style.display = 'block';
+
+        console.log('菜单位置 - 原始:', x, y, '调整后:', left, top);
+
+        options.forEach((option) => {
+          const item = document.createElement('div');
+          item.innerText = option.text;
+          item.style.padding = '8px 16px';
+          item.style.cursor = 'pointer';
+          item.style.fontSize = '14px';
+          item.style.color = '#374151';
+
+          item.addEventListener('mouseenter', () => {
+            item.style.background = '#f3f4f6';
+          });
+
+          item.addEventListener('mouseleave', () => {
+            item.style.background = 'white';
+          });
+
+          item.addEventListener('click', () => {
+            option.action();
+            // 直接移除当前菜单
+            const menuToRemove = document.getElementById(`menu_${imageId}`);
+            if (window._safeRemoveElement) {
+              window._safeRemoveElement(menuToRemove);
+            }
+          });
+
+          menu.appendChild(item);
+        });
+
+        document.body.appendChild(menu);
+        console.log('菜单已添加到 DOM,父元素:', menu.parentElement?.tagName);
+
+        // 点击其他地方关闭菜单
+        const closeMenu = (e) => {
+          console.log('closeMenu 被调用,目标:', e.target);
+          const currentMenu = document.getElementById(`menu_${imageId}`);
+          if (!currentMenu || !document.body.contains(currentMenu)) {
+            // 菜单已经不存在,移除监听器
+            document.removeEventListener('click', closeMenu);
+            return;
+          }
+
+          if (e.target !== currentMenu && !currentMenu.contains(e.target)) {
+            // 使用全局安全函数移除菜单
+            if (window._safeRemoveElement) {
+              window._safeRemoveElement(currentMenu);
+            }
+            document.removeEventListener('click', closeMenu);
+          }
+        };
+
+        setTimeout(() => {
+          console.log('添加全局点击监听器');
+          document.addEventListener('click', closeMenu);
+        }, 0);
+      },
+
+      // 调整图片大小
+      resizeImage(imageId, factor) {
+        const imageIndex = this.droppedImages.findIndex(
+          (img) => img.id === imageId
+        );
+        if (imageIndex === -1) return;
+
+        const image = this.droppedImages[imageIndex];
+        image.width *= factor;
+        image.height *= factor;
+
+        this.redrawCanvas();
+      },
+
+      // 处理 Canvas 右键点击
+      handleCanvasRightClick(e) {
+        console.log('Canvas 右键点击事件触发');
+
+        const canvas = document.getElementById('outputCanvas');
+        if (!canvas) {
+          console.log('Canvas 元素不存在');
+          return;
+        }
+
+        console.log(
+          'Canvas 元素存在,droppedImages 数量:',
+          this.droppedImages.length
+        );
+
+        if (this.droppedImages.length === 0) {
+          console.log('没有已添加的图片');
+          return;
+        }
+
+        const rect = canvas.getBoundingClientRect();
+        console.log('Canvas 边界矩形:', rect);
+
+        const scaleX = canvas.width / rect.width;
+        const scaleY = canvas.height / rect.height;
+
+        // 计算点击位置相对于 Canvas 的坐标
+        const clickX = (e.clientX - rect.left) * scaleX;
+        const clickY = (e.clientY - rect.top) * scaleY;
+
+        console.log('右键点击位置 - 屏幕:', e.clientX, e.clientY);
+        console.log('右键点击位置 - Canvas相对:', clickX, clickY);
+
+        // 查找点击到的图片
+        let clickedImage = null;
+        for (const image of this.droppedImages) {
+          console.log(
+            '检查图片:',
+            image.id,
+            '位置:',
+            image.x,
+            image.y,
+            '尺寸:',
+            image.width,
+            image.height
+          );
+          if (
+            clickX >= image.x &&
+            clickX <= image.x + image.width &&
+            clickY >= image.y &&
+            clickY <= image.y + image.height
+          ) {
+            clickedImage = image;
+            break;
+          }
+        }
+
+        if (clickedImage) {
+          console.log('点击到图片:', clickedImage.id);
+          this.showImageControls(clickedImage.id, e.clientX, e.clientY);
+        } else {
+          console.log('未点击到任何图片');
+          // 可以在这里添加其他右键功能,比如添加新图片等
+        }
+      },
+
+      // 开始移动图片
+      startMoveImage(imageId, menuX, menuY) {
+        console.log('开始移动图片,图片ID:', imageId);
+
+        // 关闭菜单
+        this.safeRemoveElement(`menu_${imageId}`);
+
+        const canvas = document.getElementById('outputCanvas');
+        if (!canvas) {
+          console.error('Canvas 元素未找到');
+          return;
+        }
+
+        const imageIndex = this.droppedImages.findIndex(
+          (img) => img.id === imageId
+        );
+        if (imageIndex === -1) {
+          console.error('图片未找到,ID:', imageId);
+          return;
+        }
+
+        const image = this.droppedImages[imageIndex];
+        console.log('找到图片:', image);
+
+        // 创建一个临时指示器
+        const indicator = document.createElement('div');
+        indicator.id = `move_indicator_${imageId}`;
+        indicator.style.position = 'fixed';
+        indicator.style.left = `${menuX}px`;
+        indicator.style.top = `${menuY}px`;
+        indicator.style.width = '40px';
+        indicator.style.height = '40px';
+        indicator.style.background = 'rgba(59, 130, 246, 0.3)';
+        indicator.style.border = '2px dashed #3b82f6';
+        indicator.style.borderRadius = '50%';
+        indicator.style.zIndex = '3000';
+        indicator.style.pointerEvents = 'none';
+        document.body.appendChild(indicator);
+        console.log('指示器已创建并添加到DOM');
+
+        // 显示提示
+        this.$message({
+          showClose: true,
+          message: '📍 移动鼠标调整位置,释放鼠标完成移动',
+          type: 'info'
+        });
+        // this.showMessage('📍 移动鼠标调整位置,释放鼠标完成移动', 'info');
+
+        const moveHandler = (e) => {
+          console.log('moveHandler 被调用,鼠标位置:', e.clientX, e.clientY);
+          const rect = canvas.getBoundingClientRect();
+          const scaleX = canvas.width / rect.width;
+          const scaleY = canvas.height / rect.height;
+
+          // 计算相对于 canvas 的位置
+          const newX = (e.clientX - rect.left) * scaleX - image.width / 2;
+          const newY = (e.clientY - rect.top) * scaleY - image.height / 2;
+
+          // 限制在 canvas 范围内
+          image.x = Math.max(0, Math.min(newX, canvas.width - image.width));
+          image.y = Math.max(0, Math.min(newY, canvas.height - image.height));
+
+          // 更新指示器位置
+          indicator.style.left = `${e.clientX - 20}px`;
+          indicator.style.top = `${e.clientY - 20}px`;
+
+          // 使用组件实例引用
+          const component = window._aaComponentInstance || this;
+          if (component && component.redrawCanvas) {
+            component.redrawCanvas();
+          }
+        };
+
+        const mouseUpHandler = (e) => {
+          console.log(
+            'mouseUpHandler 被调用,位置:',
+            e.clientX,
+            e.clientY,
+            '目标:',
+            e.target
+          );
+
+          // 无论在哪里释放鼠标,都完成移动
+          console.log('鼠标释放,完成移动');
+
+          // 确保只处理一次
+          if (indicator.parentNode) {
+            console.log('指示器仍在DOM中,移除事件监听器');
+            document.removeEventListener('mousemove', moveHandler);
+            document.removeEventListener('mouseup', mouseUpHandler);
+            indicator.remove();
+            // 使用组件实例引用
+            const component = window._aaComponentInstance || this;
+            if (component) {
+              this.$message({
+                showClose: true,
+                message: '图片已移动',
+                type: 'success'
+              });
+              // component.showMessage('✅ 图片已移动', 'success');
+            }
+          } else {
+            console.log('指示器已不在DOM中');
+          }
+        };
+
+        // 确保事件只绑定一次
+        console.log('绑定事件监听器');
+        document.removeEventListener('mousemove', moveHandler);
+        document.removeEventListener('mouseup', mouseUpHandler);
+        document.addEventListener('mousemove', moveHandler);
+        document.addEventListener('mouseup', mouseUpHandler);
+      },
+
+      // 删除图片
+      removeImage(imageId) {
+        const imageIndex = this.droppedImages.findIndex(
+          (img) => img.id === imageId
+        );
+        if (imageIndex === -1) return;
+
+        this.droppedImages.splice(imageIndex, 1);
+
+        // 删除控制菜单
+        this.safeRemoveElement(`menu_${imageId}`);
+
+        this.redrawCanvas();
+        this.$message({
+          showClose: true,
+          message: '🗑️ 图片已删除',
+          type: 'success'
+        });
+      },
+
+      // 渲染Canvas核心函数
+      async renderToCanvas() {
+        const targetElement = document.getElementById('captureTarget');
+        if (!targetElement) {
+          console.warn('未找到目标元素');
+          return;
+        }
+
+        // 显示加载状态
+        const canvasResultDiv = document.querySelector('.canvas-result');
+        const originalMsg = canvasResultDiv.querySelector('p');
+        if (originalMsg) originalMsg.style.display = 'none';
+
+        // 添加加载提示
+        let loadingMsg = document.getElementById('tempLoadingMsg');
+        if (!loadingMsg) {
+          loadingMsg = document.createElement('div');
+          loadingMsg.id = 'tempLoadingMsg';
+          loadingMsg.innerText = '⏳ 渲染中,请稍候...';
+          loadingMsg.style.padding = '12px';
+          loadingMsg.style.color = '#2563eb';
+          loadingMsg.style.fontSize = '14px';
+          canvasResultDiv.appendChild(loadingMsg);
+        }
+
+        try {
+          // 使用 html2canvas 进行转换
+          const canvas = await html2canvas(targetElement, {
+            scale: 1, // 1倍分辨率,避免过大
+            backgroundColor: '#ffffff',
+            useCORS: true, // 尝试跨域图片
+            logging: false,
+            allowTaint: false,
+            imageTimeout: 5000,
+            windowWidth: targetElement.scrollWidth,
+            windowHeight: targetElement.scrollHeight
+          });
+
+          // 移除加载提示
+          const temp = document.getElementById('tempLoadingMsg');
+          if (temp) temp.remove();
+
+          // 获取输出canvas元素并绘制
+          const outputCanvas = document.getElementById('outputCanvas');
+          outputCanvas.width = canvas.width;
+          outputCanvas.height = canvas.height;
+          // 移除内联样式,使用实际像素尺寸
+          outputCanvas.style.width = canvas.width + 'px';
+          outputCanvas.style.height = canvas.height + 'px';
+          const ctx = outputCanvas.getContext('2d');
+          ctx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
+          ctx.drawImage(canvas, 0, 0);
+
+          // 保存基础内容的 Canvas 数据(用于快速重绘)
+          this.baseCanvasData = canvas;
+
+          this.canvasGenerated = true;
+
+          // 设置 Canvas 拖放区域和事件监听
+          this.setupCanvasDropZone();
+
+          // 显示成功提示
+          // this.showMessage('✅ 渲染成功!可下载图片', 'success');
+        } catch (error) {
+          console.error('html2canvas 渲染失败: ', error);
+          const temp = document.getElementById('tempLoadingMsg');
+          if (temp) temp.remove();
+          this.canvasGenerated = false;
+          // this.showMessage('⚠️ 渲染失败,请检查HTML结构或外部资源', 'error');
+        }
+      },
+
+      // 导出为 PDF(使用 jsPDF)
+      exportToPDF() {
+        const outputCanvas = document.getElementById('outputCanvas');
+        if (
+          !this.canvasGenerated ||
+          !outputCanvas ||
+          outputCanvas.width === 0
+        ) {
+          // this.showMessage(
+          //   '请先点击"渲染至Canvas"生成图像后再导出PDF',
+          //   'error'
+          // );
+          return;
+        }
+
+        try {
+          const imgData = outputCanvas.toDataURL('image/png');
+          const canvasWidth = outputCanvas.width;
+          const canvasHeight = outputCanvas.height;
+
+          // 创建 PDF 实例,横向或纵向根据 Canvas 尺寸决定
+          const orientation = canvasWidth > canvasHeight ? 'l' : 'p';
+          const pdf = new jsPDF(orientation, 'px', [canvasWidth, canvasHeight]);
+
+          // 将 Canvas 图片添加到 PDF
+          pdf.addImage(imgData, 'PNG', 0, 0, canvasWidth, canvasHeight);
+
+          // 获取 PDF 文件流并触发事件
+          const pdfBlob = pdf.output('blob');
+          const pdfFile = new File([pdfBlob], `canvas-${Date.now()}.pdf`, {
+            type: 'application/pdf'
+          });
+
+          // 触发 PDF 生成事件,父组件可以监听此事件
+          this.$emit('pdf-generated', pdfFile);
+
+          // // 保存 PDF
+          // const timestamp = new Date()
+          //   .toISOString()
+          //   .slice(0, 19)
+          //   .replace(/:/g, '-');
+          // pdf.save(`canvas-${timestamp}.pdf`);
+
+          // this.showMessage('✅ PDF 导出成功', 'success');
+        } catch (err) {
+          console.error('PDF 导出失败', err);
+          // this.showMessage('PDF 导出失败,请稍后重试', 'error');
+        }
+      },
+
+      // 重新渲染基础内容(当 HTML 内容变化时调用)
+      async refreshBaseContent() {
+        try {
+          const targetElement = document.getElementById('captureTarget');
+          if (!targetElement) return;
+
+          const newCanvas = await html2canvas(targetElement, {
+            scale: 1,
+            backgroundColor: '#ffffff',
+            useCORS: true,
+            logging: false,
+            allowTaint: false,
+            imageTimeout: 5000,
+            width: 1000,
+            windowWidth: targetElement.scrollWidth,
+            windowHeight: targetElement.scrollHeight
+          });
+
+          this.baseCanvasData = newCanvas;
+
+          // 更新主 Canvas
+          const outputCanvas = document.getElementById('outputCanvas');
+          if (outputCanvas && this.canvasGenerated) {
+            const ctx = outputCanvas.getContext('2d');
+            ctx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
+            ctx.drawImage(newCanvas, 0, 0);
+
+            // 重新绘制所有拖入的图片
+            this.droppedImages.forEach((image) => {
+              const img = new Image();
+              img.onload = () => {
+                ctx.drawImage(img, image.x, image.y, image.width, image.height);
+              };
+              img.src = image.imgUrl;
+            });
+          }
+
+          console.log('基础内容已更新');
+        } catch (error) {
+          console.error('更新基础内容失败:', error);
+        }
+      },
+
+      // 获取 PDF 文件流(用于直接上传)
+      getPDFFile() {
+        const outputCanvas = document.getElementById('outputCanvas');
+        if (
+          !this.canvasGenerated ||
+          !outputCanvas ||
+          outputCanvas.width === 0
+        ) {
+          // this.showMessage(
+          //   '请先点击"渲染至Canvas"生成图像后再获取PDF',
+          //   'error'
+          // );
+          return Promise.reject(new Error('Canvas 未生成'));
+        }
+
+        return new Promise((resolve, reject) => {
+          try {
+            const imgData = outputCanvas.toDataURL('image/png');
+            const canvasWidth = outputCanvas.width;
+            const canvasHeight = outputCanvas.height;
+
+            // 创建 PDF 实例
+            const orientation = canvasWidth > canvasHeight ? 'l' : 'p';
+            const pdf = new jsPDF(orientation, 'px', [
+              canvasWidth,
+              canvasHeight
+            ]);
+
+            // 将 Canvas 图片添加到 PDF
+            pdf.addImage(imgData, 'PNG', 0, 0, canvasWidth, canvasHeight);
+
+            // 获取 PDF 文件流
+            const pdfBlob = pdf.output('blob');
+            const pdfFile = new File([pdfBlob], `canvas-${Date.now()}.pdf`, {
+              type: 'application/pdf'
+            });
+
+            resolve(pdfFile);
+          } catch (err) {
+            console.error('获取 PDF 文件流失败', err);
+            reject(err);
+          }
+        });
+      },
+
+      // 打印 Canvas / 另存为 PDF
+      printCanvas() {
+        const outputCanvas = document.getElementById('outputCanvas');
+        if (
+          !this.canvasGenerated ||
+          !outputCanvas ||
+          outputCanvas.width === 0
+        ) {
+          // this.showMessage('请先点击"渲染至Canvas"生成图像后再打印', 'error');
+          return;
+        }
+
+        try {
+          // 创建新窗口用于打印
+          const printWindow = window.open('', '_blank');
+          const dataUrl = outputCanvas.toDataURL('image/png');
+
+          printWindow.document.write(`
+            <!DOCTYPE html>
+            <html>
+            <head>
+              <title>打印图片</title>
+              <style>
+                body {
+                  margin: 0;
+                  padding: 20px;
+                  display: flex;
+                  justify-content: center;
+                  align-items: center;
+                }
+                img {
+                  max-width: 100%;
+                  height: auto;
+                }
+                @media print {
+                  body {
+                    padding: 0;
+                  }
+                  img {
+                    max-width: 100%;
+                    page-break-inside: avoid;
+                  }
+                }
+              </style>
+            </head>
+            <body>
+              <img src="${dataUrl}" onload="window.print(); window.close();" />
+            </body>
+            </html>
+          `);
+          printWindow.document.close();
+
+          // this.showMessage('✅ 已打开打印窗口,可选择另存为PDF', 'success');
+        } catch (err) {
+          console.error('打印失败', err);
+          // this.showMessage('打印失败,请检查浏览器设置', 'error');
+        }
+      },
+
+      init(html) {
+        this.editableHtml = html;
+        this.previewHtml = html;
+        this.$nextTick(() => {
+          this.renderToCanvas();
+        });
+      }
+    }
+  };
+</script>
+
+<style scoped>
+  * {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+  }
+
+  .app-container {
+    max-width: 1400px;
+    width: 100%;
+    margin: 0 auto;
+    padding: 24px 20px;
+    min-height: 100vh;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background: linear-gradient(145deg, #e0eafc 0%, #cfdef3 100%);
+  }
+
+  .tool-card {
+    background: rgba(255, 255, 255, 0.92);
+    backdrop-filter: blur(2px);
+    border-radius: 2rem;
+    box-shadow: 0 25px 45px -12px rgba(0, 0, 0, 0.3),
+      0 1px 2px rgba(0, 0, 0, 0.05);
+    overflow: hidden;
+    padding: 1.8rem 2rem 2rem 2rem;
+    width: 100%;
+  }
+
+  h1 {
+    font-size: 2rem;
+    font-weight: 700;
+    background: linear-gradient(135deg, #1a2a6c, #3b82f6, #8b5cf6);
+    background-clip: text;
+    -webkit-background-clip: text;
+    color: transparent;
+    letter-spacing: -0.3px;
+    margin-bottom: 0.35rem;
+  }
+
+  .sub {
+    color: #4a5568;
+    border-left: 4px solid #3b82f6;
+    padding-left: 14px;
+    margin-bottom: 1.8rem;
+    font-weight: 500;
+    font-size: 0.95rem;
+  }
+
+  .converter-layout {
+    display: flex;
+    justify-content: center;
+  }
+
+  .editor-panel {
+    flex: 1.2;
+    min-width: 280px;
+    background: #f8fafc;
+    border-radius: 1.5rem;
+    padding: 1.25rem;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+    border: 1px solid #e2e8f0;
+  }
+
+  .panel-title {
+    font-weight: 600;
+    font-size: 1.2rem;
+    color: #0f172a;
+    margin-bottom: 1rem;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    border-bottom: 2px solid #cbd5e1;
+    padding-bottom: 8px;
+  }
+
+  .html-editor {
+    width: 100%;
+    height: 360px;
+    background: #1e293b;
+    color: #e2e8f0;
+    font-family: 'Fira Code', 'Cascadia Code', monospace;
+    font-size: 13px;
+    line-height: 1.5;
+    padding: 1rem;
+    border-radius: 1rem;
+    border: none;
+    resize: vertical;
+    outline: none;
+    margin-bottom: 1rem;
+    box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2);
+  }
+
+  .html-editor:focus {
+    border: 1px solid #3b82f6;
+    background: #0f172a;
+  }
+
+  .toolbar {
+    display: flex;
+    gap: 12px;
+    flex-wrap: wrap;
+    margin-top: 8px;
+    margin-bottom: 16px;
+  }
+
+  button {
+    background: white;
+    border: 1px solid #cbd5e1;
+    padding: 8px 18px;
+    border-radius: 40px;
+    font-weight: 600;
+    font-size: 0.85rem;
+    cursor: pointer;
+    transition: 0.2s;
+    color: #1e293b;
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+  }
+
+  button.primary {
+    background: #3b82f6;
+    border-color: #3b82f6;
+    color: white;
+    box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
+  }
+
+  button.primary:hover {
+    background: #2563eb;
+    transform: translateY(-1px);
+  }
+
+  button.download {
+    background: #10b981;
+    border-color: #10b981;
+    color: white;
+  }
+
+  button.download:hover {
+    background: #059669;
+    transform: translateY(-1px);
+  }
+
+  button.pdf {
+    background: #8b5cf6;
+    border-color: #8b5cf6;
+    color: white;
+  }
+
+  button.pdf:hover {
+    background: #7c3aed;
+    transform: translateY(-1px);
+  }
+
+  button.print {
+    background: #8b5cf6;
+    border-color: #8b5cf6;
+    color: white;
+  }
+
+  button.print:hover {
+    background: #7c3aed;
+    transform: translateY(-1px);
+  }
+
+  .canvas-buttons {
+    display: flex;
+    gap: 12px;
+    justify-content: center;
+    margin-top: 16px;
+  }
+
+  button:hover {
+    background: #f1f5f9;
+    transform: translateY(-1px);
+  }
+
+  .info-message {
+    font-size: 0.8rem;
+    color: #2563eb;
+    background: #eff6ff;
+    padding: 6px 12px;
+    border-radius: 40px;
+    display: inline-block;
+  }
+
+  .preview-panel {
+    flex: 1;
+    min-width: 0;
+    background: #ffffff;
+    border-radius: 1.5rem;
+    padding: 1.25rem;
+    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
+    border: 1px solid #eef2ff;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .images-panel {
+    flex: 0 0 300px;
+    background: #ffffff;
+    border-radius: 1.5rem;
+    padding: 1.25rem;
+    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
+    border: 1px solid #eef2ff;
+    display: flex;
+    flex-direction: column;
+    position: fixed;
+    right: 2rem;
+    top: 40px;
+    max-height: calc(100vh - 80px);
+    overflow-y: auto;
+    z-index: 100;
+  }
+
+  .seal-section {
+    margin-bottom: 1rem;
+  }
+
+  .seal-section:last-child {
+    margin-bottom: 0;
+  }
+
+  .no-data {
+    grid-column: 1 / -1;
+    text-align: center;
+    color: #94a3b8;
+    padding: 20px 0;
+    font-size: 14px;
+  }
+
+  .images-grid {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 12px;
+    /* flex: 1; */
+    overflow-y: auto;
+  }
+
+  .drag-item {
+    background: #f8fafc;
+    border: 2px solid #e2e8f0;
+    border-radius: 12px;
+    padding: 8px;
+    cursor: move;
+    transition: all 0.2s;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    height: 130px; /* 固定高度:图片80px + 内边距8px*2 + 文字区域 */
+    justify-content: space-between;
+  }
+
+  .drag-item:hover {
+    border-color: #3b82f6;
+    background: #eff6ff;
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
+  }
+
+  .drag-item img {
+    width: 100%;
+    height: 80px;
+    object-fit: contain; /* 改为contain,确保完整显示图片 */
+    border-radius: 8px;
+    pointer-events: none;
+    background-color: #f1f5f9; /* 添加背景色,防止透明图片看不清 */
+  }
+
+  .drag-item-name {
+    font-size: 12px;
+    color: #64748b;
+    margin-top: 6px;
+    text-align: center;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    width: 100%;
+    line-height: 1.4;
+    padding: 0 4px;
+    max-height: 32px; /* 限制文字最大高度 */
+  }
+
+  .preview-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: baseline;
+    margin-bottom: 1rem;
+    flex-wrap: wrap;
+  }
+
+  .badge {
+    background: #e6f0ff;
+    padding: 4px 12px;
+    border-radius: 30px;
+    font-size: 0.75rem;
+    font-weight: 500;
+    color: #1e40af;
+  }
+
+  .canvas-area {
+    background: #f1f5f9;
+    border-radius: 1.2rem;
+    padding: 1rem;
+    text-align: center;
+    min-height: 280px;
+  }
+
+  .target-element {
+    background: white;
+    border-radius: 1rem;
+    padding: 1rem;
+    box-shadow: 0 6px 14px rgba(0, 0, 0, 0.05);
+    max-width: 100%;
+    overflow: auto;
+  }
+
+  hr {
+    margin: 0.8rem 0;
+    border-color: #e2e8f0;
+  }
+
+  .canvas-result {
+    margin-top: 1rem;
+    background: #ffffffd9;
+    border-radius: 1rem;
+    padding: 0.8rem;
+    text-align: center;
+  }
+
+  .canvas-result p {
+    font-size: 0.8rem;
+    color: #475569;
+  }
+
+  canvas {
+    border-radius: 12px;
+    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
+    background: white;
+  }
+
+  footer {
+    margin-top: 1.5rem;
+    text-align: center;
+    font-size: 0.75rem;
+    color: #334155;
+  }
+
+  @media (max-width: 780px) {
+    .tool-card {
+      padding: 1rem;
+    }
+    .html-editor {
+      height: 260px;
+    }
+    .converter-layout {
+      justify-content: center;
+      /* flex-wrap: wrap;
+      padding-right: 0;
+      gap: 1rem; */
+    }
+    .images-panel {
+      position: relative;
+      right: auto;
+      top: auto;
+      width: 100%;
+      max-height: 300px; /* 在小屏幕下限制最大高度 */
+      order: 2; /* 在移动端将图片区域放在底部 */
+      margin-top: 1rem;
+    }
+    .drag-item {
+      height: 120px; /* 在小屏幕下稍微减小高度 */
+    }
+    .drag-item img {
+      height: 70px; /* 在小屏幕下减小图片高度 */
+    }
+  }
+</style>

+ 105 - 0
src/components/addDoc/sealManagement.vue

@@ -0,0 +1,105 @@
+<template>
+  <ele-modal
+    :visible.sync="showEditFlag"
+    :close-on-click-modal="false"
+    custom-class="ele-dialog-form"
+    append-to-body
+    :fullscreen="true"
+  >
+    <iframe
+      :src="fileUrl"
+      width="100%"
+      v-if="showEditFlag"
+      style="height: calc(100vh - 100px); position: absolute; left: -1000000px"
+      frameborder="0"
+      allowfullscreen="true"
+      id="Iframe"
+    ></iframe>
+
+    <seal ref="sealRef" @pdf-generated="pdfGenerated"></seal>
+
+    <template slot="footer">
+      <el-button
+        type="primary"
+        size="small"
+        @click="handleSave"
+        :disabled="false"
+        >保存</el-button
+      >
+      <el-button type="primary" size="small" @click="restore" :disabled="false"
+        >还原</el-button
+      >
+      <el-button size="small" @click="showEditFlag = false" :disabled="false"
+        >取消</el-button
+      >
+    </template>
+  </ele-modal>
+</template>
+
+<script>
+  import seal from './seal.vue';
+  import { uploadFileNew } from './api';
+  export default {
+    data() {
+      return {
+        fileUrl: '',
+        showEditFlag: false,
+        restoreData: null
+      };
+    },
+    components: { seal },
+
+    methods: {
+      open(row) {
+        this.showEditFlag = true;
+        this.restoreData = row;
+
+        this.setFileUrl(row);
+      },
+      restore() {
+        this.setFileUrl(this.restoreData);
+      },
+      handleSave() {
+        this.$refs.sealRef.exportToPDF();
+      },
+      pdfGenerated(data) {
+        uploadFileNew({
+          module: 'fm',
+          multiPartFile: data
+        })
+          .then((res) => {
+            this.$emit('save', {
+              stampStoragePath: res.data ? [res.data] : [],
+              id: this.restoreData.id
+            });
+          })
+          .finally(() => {
+            this.loading = false;
+          });
+      },
+      setFileUrl(row) {
+        let file = row.stampStoragePath[0] || row.storagePath[0];
+        let fileNames = file.storePath.split('/');
+        let url =
+          window.location.origin +
+          '/api/main/file/getFile?objectName=' +
+          file.storePath +
+          '&fullfilename=' +
+          fileNames[fileNames.length - 1];
+        this.fileUrl = '/kkfile/onlinePreview?url=' + btoa(url);
+        this.$nextTick(() => {
+          var iframe = document.getElementById('Iframe');
+          var iframeDocument = iframe.contentWindow.document;
+          var container = iframeDocument.querySelectorAll('.container');
+          this.$refs.sealRef.init(
+            container[0].innerHTML.replace(
+              /src="https?:\/\/[^\/]+(\/[^"]*)"/g,
+              `src="${window.location.origin}$1"`
+            )
+          );
+        });
+      }
+    }
+  };
+</script>
+<style scoped lang="scss"></style>

+ 151 - 151
src/views/bpm/handleTask/components/doc/send/sendDialog.vue

@@ -78,7 +78,7 @@
                 icon="el-icon-edit"
                 @click="browseOpen(row)"
               >
-                浏览
+                签章
               </el-link>
             </template>
           </ele-pro-table>
@@ -95,165 +95,165 @@
         </el-form-item>
       </el-col>
     </el-row>
-    <browse ref="browseRef"></browse>
+    <browse ref="browseRef" @save="save"></browse>
   </el-form>
 </template>
-  
-  <script>
-import power from './power.vue';
-import { sendGetById } from '@/api/bpm/components/doc';
-import browse from '@/components/addDoc/browse.vue';
 
-const defaultForm = {
-  name: '', //名称
-  fileList: [], //文档集合json
-  userAuthority: [], //用户权限集合json
-  releaseTime: '', //发布时间
-  failureTime: '', //失效时间
-  isAuthority: '', //是否回收权限 0 1
-  remark: ''
-};
-export default {
-  components: { power, browse },
-  props: {
-    businessId: {
-      default: ''
-    }
-  },
-  data() {
-    return {
-      form: {
-        ...defaultForm
-      },
-
-      columns: [
-        {
-          label: '编码',
-          prop: 'code',
-          width: 180,
-          align: 'center',
-          fixed: 'left',
-          showOverflowTooltip: true
-        },
-        {
-          prop: 'name',
-          label: '文档名称',
-          align: 'center',
-          slot: 'name',
-          showOverflowTooltip: true,
-          minWidth: 200
+<script>
+  import power from './power.vue';
+  import { sendGetById, fileUpdateAPI } from '@/api/bpm/components/doc';
+  import browse from '@/components/addDoc/sealManagement.vue';
+  const defaultForm = {
+    name: '', //名称
+    fileList: [], //文档集合json
+    userAuthority: [], //用户权限集合json
+    releaseTime: '', //发布时间
+    failureTime: '', //失效时间
+    isAuthority: '', //是否回收权限 0 1
+    remark: ''
+  };
+  export default {
+    components: { power, browse },
+    props: {
+      businessId: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        form: {
+          ...defaultForm
         },
-        {
-          prop: 'storagePath',
-          label: '文件名称',
-          align: 'center',
 
-          showOverflowTooltip: true,
-          minWidth: 200,
-          formatter: (_row, _column, cellValue) => {
-            return cellValue[0]?.name;
-          }
-        },
-        {
-          prop: 'version',
-          label: '版本',
-          align: 'center',
-          showOverflowTooltip: true,
-          minWidth: 100
-        },
+        columns: [
+          {
+            label: '编码',
+            prop: 'code',
+            width: 180,
+            align: 'center',
+            fixed: 'left',
+            showOverflowTooltip: true
+          },
+          {
+            prop: 'name',
+            label: '文档名称',
+            align: 'center',
+            slot: 'name',
+            showOverflowTooltip: true,
+            minWidth: 200
+          },
+          {
+            prop: 'storagePath',
+            label: '文件名称',
+            align: 'center',
 
-        {
-          prop: 'createUserName',
-          label: '创建人',
-          align: 'center',
-          showOverflowTooltip: true,
-          minWidth: 100
-        },
-        {
-          prop: 'createTime',
-          label: '创建时间',
-          align: 'center',
-          showOverflowTooltip: true,
-          minWidth: 160,
-          formatter: (_row, _column, cellValue) => {
-            return this.$util.toDateString(cellValue);
-          }
-        },
-        {
-          prop: 'updateUserName',
-          label: '修改人',
-          align: 'center',
-          showOverflowTooltip: true,
-          minWidth: 100
-        },
-        {
-          prop: 'updateTime',
-          label: '修改时间',
-          align: 'center',
-          showOverflowTooltip: true,
-          minWidth: 160
-        },
-        {
-          prop: 'sizeUnit',
-          label: '文档大小',
-          align: 'center',
-          showOverflowTooltip: true,
-          minWidth: 100
-        },
-        {
-          prop: '',
-          label: '状态',
-          align: 'center',
-          showOverflowTooltip: true,
-          minWidth: 100
-        },
-        {
-          columnKey: 'action',
-          label: '操作',
-          width: 150,
-          align: 'center',
-          resizable: false,
-          slot: 'action',
-          showOverflowTooltip: true,
-          fixed: 'right'
-        }
-      ],
+            showOverflowTooltip: true,
+            minWidth: 200,
+            formatter: (_row, _column, cellValue) => {
+              return cellValue[0]?.name;
+            }
+          },
+          {
+            prop: 'version',
+            label: '版本',
+            align: 'center',
+            showOverflowTooltip: true,
+            minWidth: 100
+          },
 
-      powerArr: [
-        { name: 'visible', label: '可见' },
-        { name: 'check', label: '查看' },
-        { name: 'browse', label: '浏览' },
+          {
+            prop: 'createUserName',
+            label: '创建人',
+            align: 'center',
+            showOverflowTooltip: true,
+            minWidth: 100
+          },
+          {
+            prop: 'createTime',
+            label: '创建时间',
+            align: 'center',
+            showOverflowTooltip: true,
+            minWidth: 160,
+            formatter: (_row, _column, cellValue) => {
+              return this.$util.toDateString(cellValue);
+            }
+          },
+          {
+            prop: 'updateUserName',
+            label: '修改人',
+            align: 'center',
+            showOverflowTooltip: true,
+            minWidth: 100
+          },
+          {
+            prop: 'updateTime',
+            label: '修改时间',
+            align: 'center',
+            showOverflowTooltip: true,
+            minWidth: 160
+          },
+          {
+            prop: 'sizeUnit',
+            label: '文档大小',
+            align: 'center',
+            showOverflowTooltip: true,
+            minWidth: 100
+          },
+          {
+            prop: '',
+            label: '状态',
+            align: 'center',
+            showOverflowTooltip: true,
+            minWidth: 100
+          },
+          {
+            columnKey: 'action',
+            label: '操作',
+            width: 150,
+            align: 'center',
+            resizable: false,
+            slot: 'action',
+            showOverflowTooltip: true,
+            fixed: 'right'
+          }
+        ],
 
-        { name: 'download', label: '下载' },
-        { name: 'print', label: '打印' }
-      ]
-    };
-  },
-  computed: {
-    // 是否开启响应式布局
-    styleResponsive() {
-      return this.$store.state.theme.styleResponsive;
-    }
-  },
-  created() {
-    this.init();
-  },
-  methods: {
-    async init() {
-      const data = await sendGetById(this.businessId);
-      this.form = data;
+        powerArr: [
+          { name: 'visible', label: '可见' },
+          { name: 'check', label: '查看' },
+          { name: 'browse', label: '浏览' },
 
-      this.$nextTick(() => {
-        this.$refs.powerRef &&
-          this.$refs.powerRef.setTableList(data.userAuthority);
-      });
+          { name: 'download', label: '下载' },
+          { name: 'print', label: '打印' }
+        ]
+      };
     },
-    browseOpen(row) {
-      this.$refs.browseRef.open(row);
+    computed: {
+      // 是否开启响应式布局
+      styleResponsive() {
+        return this.$store.state.theme.styleResponsive;
+      }
+    },
+    created() {
+      this.init();
+    },
+    methods: {
+      save(data) {
+        fileUpdateAPI(data);
+      },
+      async init() {
+        const data = await sendGetById(this.businessId);
+        this.form = data;
+
+        this.$nextTick(() => {
+          this.$refs.powerRef &&
+            this.$refs.powerRef.setTableList(data.userAuthority);
+        });
+      },
+      browseOpen(row) {
+        this.$refs.browseRef.open(row);
+      }
     }
-  }
-};
+  };
 </script>
-  <style scoped lang="scss">
-</style>
-  
+<style scoped lang="scss"></style>

+ 317 - 0
src/views/bpm/handleTask/components/sealManagement/sealDialog.vue

@@ -0,0 +1,317 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="form"
+    :rules="rules"
+    label-width="120px"
+    size="small"
+  >
+    <!-- 第一行:印章编码 + 印章名称 -->
+    <el-row :gutter="20">
+      <el-col :span="12">
+        <el-form-item label="印章编码" prop="code">
+          <el-input
+            v-model="form.code"
+            placeholder="系统自动生成,唯一码"
+            disabled
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="印章名称" prop="name" required>
+          <el-input
+            v-model="form.name"
+            placeholder="请输入"
+            disabled
+            clearable
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <!-- 第二行:印章类型 + 印章编号 -->
+    <el-row :gutter="20">
+      <el-col :span="12">
+        <el-form-item label="印章类型" prop="sealType" required>
+          <el-select
+            v-model="form.sealType"
+            placeholder="请选择"
+            clearable
+            style="width: 100%"
+            disabled
+          >
+            <el-option
+              v-for="item in sealTypeList"
+              :label="item.label"
+              :key="item.value"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="印章编号" prop="sealNumber">
+          <el-input
+            v-model="form.sealNumber"
+            placeholder="请输入"
+            disabled
+            clearable
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <!-- 第三行:印章持有人 + 版本 -->
+    <el-row :gutter="20">
+      <el-col :span="12">
+        <el-form-item label="印章持有人" prop="sealHolderName">
+          <el-input
+            :maxlength="20"
+            v-model="form.sealHolderName"
+            @click.native="setUsers"
+            placeholder="选择人员"
+            disabled
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="版本" prop="version" required>
+          <el-input
+            v-model="form.version"
+            placeholder="请输入"
+            disabled
+            clearable
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <!-- 第四行:是否长期有效 + 状态 -->
+    <el-row :gutter="20">
+      <el-col :span="12">
+        <el-form-item label="是否长期有效" prop="isLongTerm" required>
+          <el-radio-group v-model="form.isLongTerm" disabled>
+            <el-radio :label="1">长期</el-radio>
+            <el-radio :label="0">非长期</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="状态" prop="status" required>
+          <el-switch
+            v-model="form.status"
+            :active-value="1"
+            :inactive-value="0"
+            disabled
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <!-- 非长期时显示有效期 -->
+    <el-row :gutter="20" v-if="form.isLongTerm == '0'">
+      <el-col :span="12">
+        <el-form-item label="有效期开始日期" prop="validStartDate">
+          <el-date-picker
+            v-model="form.validStartDate"
+            disabled
+            type="date"
+            value-format="yyyy-MM-dd"
+            style="width: 100%"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="有效期结束日期" prop="validEndDate">
+          <el-date-picker
+            v-model="form.validEndDate"
+            type="date"
+            value-format="yyyy-MM-dd"
+            disabled
+            style="width: 100%"
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <!-- 印章图片上传 -->
+    <el-row>
+      <el-col :span="24">
+        <el-form-item label="印章图片" prop="imgUrl" required>
+          <div class="upload-container">
+            <el-image
+              style="max-width: 200px; max-height: 200px"
+              :src="form.imgUrl"
+              :preview-src-list="[form.imgUrl]"
+            >
+            </el-image>
+            <div class="upload-tips">
+              <p>上传要求:</p>
+              <p>1、图像样式必须与实物印章的印模规制保持一致。</p>
+              <p>2、印模必须清晰、完整、无断裂、无拖影</p>
+            </div>
+          </div>
+        </el-form-item>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+
+<script>
+  import { getSealById } from '@/api/bpm/components/sealManagement';
+
+  const defaultForm = {
+    code: '',
+    imgUrl: '',
+    isLongTerm: 1,
+    name: '',
+    remark: '',
+    sealHolderId: '',
+    sealHolderName: '',
+    sealNumber: '',
+    sealType: '',
+    sealTypeName: '',
+    status: 1,
+    processInstanceId: '',
+    validEndDate: '',
+    validStartDate: '',
+    version: ''
+  };
+
+  export default {
+    props: {
+      businessId: {
+        type: String,
+        default: ''
+      }
+    },
+    data() {
+      return {
+        sealTypeList: [
+          {
+            label: '法律效力章',
+            value: '1'
+          },
+          {
+            label: '财务专用章',
+            value: '2'
+          },
+          {
+            label: '业务专用章',
+            value: '3'
+          },
+          {
+            label: '内部管理章',
+            value: '4'
+          }
+        ],
+        form: {
+          ...defaultForm
+        },
+        rules: {
+          name: [
+            { required: true, message: '请输入印章名称', trigger: 'blur' }
+          ],
+          sealType: [
+            { required: true, message: '请选择印章类型', trigger: 'change' }
+          ],
+          version: [{ required: true, message: '请输入版本', trigger: 'blur' }],
+          isLongTerm: [
+            { required: true, message: '请选择是否长期有效', trigger: 'change' }
+          ],
+          status: [
+            { required: true, message: '请选择状态', trigger: 'change' }
+          ],
+          imgUrl: [
+            { required: true, message: '请上传印章图片', trigger: 'change' }
+          ],
+          validStartDate: [
+            { required: true, message: '请选择', trigger: 'change' }
+          ],
+          validEndDate: [
+            { required: true, message: '请选择', trigger: 'change' }
+          ]
+        }
+      };
+    },
+    created() {
+      this.init();
+    },
+    methods: {
+      init() {
+        getSealById(this.businessId).then((res) => {
+          this.form = res;
+        });
+      }
+    }
+  };
+</script>
+
+<style scoped>
+  .upload-container {
+    display: flex;
+    align-items: flex-start;
+    gap: 20px;
+  }
+
+  .seal-uploader {
+    width: 200px;
+    height: 200px;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+    background-color: #f5f7fa;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .seal-uploader:hover {
+    border-color: #409eff;
+  }
+
+  .uploader-placeholder {
+    text-align: center;
+    color: #8c939d;
+  }
+
+  .uploader-placeholder .el-icon-picture-outline {
+    font-size: 48px;
+    margin-bottom: 10px;
+  }
+
+  .upload-text {
+    display: flex;
+    gap: 10px;
+    justify-content: center;
+  }
+
+  .delete-btn {
+    color: #f56c6c;
+  }
+
+  .seal-image {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+  }
+
+  .upload-tips {
+    flex: 1;
+    font-size: 12px;
+    color: #606266;
+    line-height: 1.8;
+  }
+
+  .upload-tips p {
+    margin: 0;
+    padding: 0;
+  }
+
+  .upload-tips p:first-child {
+    font-weight: bold;
+    margin-bottom: 5px;
+  }
+</style>

+ 134 - 0
src/views/bpm/handleTask/components/sealManagement/submit.vue

@@ -0,0 +1,134 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <el-form-item
+        label="审批建议"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <el-input
+          type="textarea"
+          v-model="form.reason"
+          placeholder="请输入审批建议"
+        />
+      </el-form-item>
+    </el-form>
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        v-click-once
+        @click="handleAudit(1)"
+        >通过
+      </el-button>
+
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        v-click-once
+        @click="handleAudit(0)"
+        >驳回
+      </el-button>
+      <el-dropdown
+        @command="(command) => handleCommand(command)"
+        style="margin-left: 30px"
+      >
+        <span class="el-dropdown-link">
+          更多<i class="el-icon-arrow-down el-icon--right"></i>
+        </span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="cancel">作废</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </el-col>
+</template>
+
+<script>
+  import { approveTaskWithVariables, rejectTask,cancelTask } from '@/api/bpm/task';
+
+  export default {
+    name: '',
+
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        visible: false,
+        form: {
+          reason: '同意'
+        },
+       
+      };
+    },
+    created() {},
+    methods: {
+      async handleAudit(status) {
+        await this._approveTaskWithVariables(status);
+      },
+      //更多
+      handleCommand(command) {
+        if (command === 'cancel') {
+          this.$confirm('是否确认作废?', {
+            type: 'warning',
+            cancelButtonText: '取消',
+            confirmButtonText: '确定'
+          })
+            .then(() => {
+              cancelTask({
+                id: this.id,
+                taskId: this.taskId,
+                reason: this.form.reason,
+                businessId: this.businessId
+              })
+                .then(() => {
+                  this.$emit('handleClose');
+                })
+                .catch(() => {
+                  this.$message.error('流程作废失败');
+                });
+            })
+            .catch(() => {});
+        }
+      },
+      async _approveTaskWithVariables(status) {
+        let variables = {
+          pass: !!status
+        };
+        let API = !!status ? approveTaskWithVariables : rejectTask;
+        API({
+          id: this.taskId,
+          reason: this.form.reason,
+          variables
+        }).then((res) => {
+          if (res.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: status === 0 ? '驳回' : ''
+            });
+          }
+        });
+      }
+    }
+  };
+</script>
+
+<style lang="scss"></style>

+ 1 - 1
vue.config.js

@@ -38,7 +38,7 @@ module.exports = {
         // target: 'http://124.71.68.31:50001', // 测试环境
         // target: 'http://124.71.68.31:50001',
         // target: 'http://192.168.1.105:18086',
-        target: 'http://192.168.1.125:18086',
+        target: 'http://192.168.1.3:18086',
         // target: 'http://192.168.1.116:18086',
         // target: 'http://192.168.1.3:18086',
         // target: 'http://192.168.1.251:18186',