|
|
@@ -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>
|