From 9056f144dad76081d595dbdc8f4d16bb28859114 Mon Sep 17 00:00:00 2001 From: 24kycj <1637269896@qq.com> Date: Fri, 19 Dec 2025 19:37:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 30 +- lib/design1/core.js | 75 +- lib/design1/ui.js | 50 +- lib/design2/core.js | 4937 ++++++++++++++------------- lib/design2/output.js | 160 +- lib/design2/ui.js | 263 +- package arm64.json | 2 +- package.json | 10 +- scripts/encrypt/README.md | 4 +- scripts/encrypt/encrypt-electron.js | 2 +- 10 files changed, 2876 insertions(+), 2657 deletions(-) diff --git a/README.md b/README.md index 50e299e..c212fc8 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ ```bash # 1. 安装依赖(只需一次) -npm install +yarn install # 2. 启动程序 -npm start +yarn start ``` > **前置要求**:需要安装 [Node.js](https://nodejs.org/)(推荐 16 或更高版本) @@ -26,13 +26,13 @@ npm start ```bash # Windows 系统 -npm run release:win +yarn release:win # Mac 系统 -npm run release:mac +yarn release:mac # Linux 系统 -npm run release:linux +yarn release:linux ``` ✅ **自动完成**:加密 → 测试 → 清理 → 打包 @@ -45,8 +45,8 @@ npm run release:linux #### 步骤 1:加密代码 ```bash -npm run encrypt:win # Windows -npm run encrypt:linux # Mac/Linux +yarn encrypt:win # Windows +yarn encrypt:linux # Mac/Linux ``` ✅ 生成 `.jsc` 加密文件(自动覆盖旧文件) @@ -54,7 +54,7 @@ npm run encrypt:linux # Mac/Linux #### 步骤 2:测试 ```bash -npm start +yarn start ``` ✅ 确认程序正常运行,无报错 @@ -62,7 +62,7 @@ npm start #### 步骤 3:清理源文件 ```bash -npm run cleanup +yarn cleanup ``` ✅ 删除 `.js` 源文件,自动备份到 `.backup` 文件夹 @@ -70,9 +70,9 @@ npm run cleanup #### 步骤 4:打包 ```bash -npm run build:win # Windows -npm run build:mac # Mac -npm run build:linux # Linux +yarn build:win # Windows +yarn build:mac # Mac +yarn build:linux # Linux ``` ✅ 生成安装包到 `build` 文件夹 @@ -84,7 +84,7 @@ npm run build:linux # Linux ### 恢复源文件 ```bash -npm run restore +yarn restore ``` > 从 `.backup` 文件夹恢复所有源文件(误删除时使用) @@ -92,7 +92,7 @@ npm run restore ### 清理加密文件 ```bash -npm run cleanup:jsc +yarn cleanup:jsc ``` > 删除所有 `.jsc` 文件(重新加密前使用) @@ -113,7 +113,7 @@ npm run cleanup:jsc } ``` -然后重新运行 `npm run encrypt:win` +然后重新运行 `yarn encrypt:win` --- diff --git a/lib/design1/core.js b/lib/design1/core.js index b2d8803..9853d21 100644 --- a/lib/design1/core.js +++ b/lib/design1/core.js @@ -41,6 +41,8 @@ fabric.Object.prototype.objectCaching = false; ctx1 = canvas1.getSelectionContext(); ctx2 = canvas2.getSelectionContext(); is_bgi_add = false; +// 撤销/重做过程中使用的标志位,防止并发操作和误记录历史 +window._isApplyingHistory = false; // 同步到 window 对象 window.ctx1 = ctx1; window.ctx2 = ctx2; @@ -1224,6 +1226,10 @@ window.selectObj = function selectObj(objIndices, isFromCanvas = false) { } window.recordState = function recordState() { + // 撤销/重做过程中不记录历史,避免产生“中间状态”,导致需要点多次才能回到预期 + if (window._isApplyingHistory) { + return; + } //pre_objs.push(JSON.stringify(objs)); //pre_json.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); //next_json = []; @@ -1492,8 +1498,11 @@ function isObjectLocked(obj) { window.isObjectLocked = isObjectLocked; // 统一的 toJSON 属性列表,不包含锁定相关属性(锁定状态不应被保存) +// 统一的 toJSON 属性列表,不包含锁定相关属性(锁定状态不应被保存) +// 包含所有需要保存的属性,特别是自定义对象的属性(如 CurvedText 的 diameter, flipped) const TO_JSON_PROPERTIES = [ - "selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline", "evented" + "selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline", + "evented", "linethrough", "textBackgroundColor", "diameter", "flipped" ]; // 暴露到全局作用域 window.TO_JSON_PROPERTIES = TO_JSON_PROPERTIES; @@ -1608,13 +1617,14 @@ function initCanvasEvents() { handleObjectSelected(e); }); - // 阻止被锁定的对象被框选选中(只过滤被锁定的对象,保留未锁定的对象) + // 阻止被锁定的对象和辅助线被框选选中: + // - 锁定对象:不能被框选(只能通过列表选中) + // - 辅助线:永远不进入选区,避免选区被拉得很大 currentCanvas.on('before:selection:created', (e) => { if (!e.targets || e.targets.length === 0) return; - // 过滤掉被锁定的对象,保留辅助线和未锁定的对象 e.targets = e.targets.filter(target => { - if (target.isGuideLine) return true; // 保留辅助线 - return !isObjectLocked(target); // 移除被锁定的对象,保留未锁定的对象 + if (target.isGuideLine) return false; // 完全排除辅助线 + return !isObjectLocked(target); // 排除被锁定的普通对象 }); }); @@ -1850,6 +1860,11 @@ function initRulers() { e.preventDefault(); e.stopPropagation(); + // 防止重复调用:如果正在拖拽,直接返回 + if ($dragLine && $dragLine.length > 0) { + return; + } + // 创建临时显示的线 $dragLine = $('
'); $('.canvas-wrapper').append($dragLine); @@ -1922,18 +1937,13 @@ function initRulers() { let canvasX = relX / zoom; let canvasY = relY / zoom; - // 获取画布的实际尺寸和可见区域尺寸 - let canvasWidth = canvas.width || 10000; - let canvasHeight = canvas.height || 10000; - // 获取可见区域的尺寸(考虑缩放) - let visibleWidth = canvasWrapper.width() || canvasWidth; - let visibleHeight = canvasWrapper.height() || canvasHeight; - // 使用足够大的固定值,确保辅助线能够覆盖整个画布和可见区域 - // 使用 100000 确保无论画布多大、如何缩放,都能覆盖整个可见区域 - let lineExtent = 100000; + // 获取画布的实际尺寸(用于决定辅助线的长度) + let canvasWidth = canvas.width || 2000; + let canvasHeight = canvas.height || 2000; + // 使用与画布尺寸相关的长度,避免“整块区域都像辅助线”,同时保证能横穿整个画布 + let lineExtent = Math.max(canvasWidth, canvasHeight) * 2; - // 构造线对象,使用足够大的范围确保覆盖整个画布 - // 线的起点和终点要足够远,确保覆盖整个可见区域 + // 构造线对象 let linePoints = []; if (type === 'horizontal') { // 水平线:从很远的左边到很远的右边 @@ -1962,19 +1972,34 @@ function initRulers() { lockScalingX: true, lockScalingY: true, hoverCursor: type === 'horizontal' ? 'ns-resize' : 'ew-resize', - perPixelTargetFind: false, // 使用边界框检测而不是像素检测 - targetFindTolerance: 10, // 增加点击容差,扩大可点击区域(像素) - padding: 10, // 增加选择边界的内边距 + // design1 中改回边框命中模型,保证更容易点中拖拽 + perPixelTargetFind: false, + targetFindTolerance: 10, + padding: 10, isGuideLine: true, ignoreSave: true }); - // 限制移动方向 - guideLine.on('moving', function (e) { + // 限制移动方向(只约束方向,不把整条线拖到极远,避免“整片区域是辅助线”的感觉) + guideLine.on('moving', function () { if (type === 'horizontal') { - this.left = -lineExtent; + // 水平线:根据当前中心点的 Y,重算 x1/x2,使其横穿画布 + const cy = this.getCenterPoint().y; + this.set({ + x1: -lineExtent, + y1: cy, + x2: lineExtent, + y2: cy + }); } else { - this.top = -lineExtent; + // 垂直线:根据当前中心点的 X,重算 y1/y2,使其竖穿画布 + const cx = this.getCenterPoint().x; + this.set({ + x1: cx, + y1: -lineExtent, + x2: cx, + y2: lineExtent + }); } }); @@ -2200,8 +2225,8 @@ window.copySelection = function copySelection() { let appData = (idx > 0 && objs[idx - 1]) ? objs[idx - 1] : { type: 0 }; return { - // 1. 保存画面样式 (坐标、颜色、字号等) - fabricJson: obj.toObject(['id', 'name', 'selectable', 'evented', 'data']), + // 1. 保存画面样式 (坐标、颜色、字号等,包含图片的 src) + fabricJson: obj.toObject(['id', 'name', 'selectable', 'evented', 'data', 'src']), // 2. 保存业务属性 (深拷贝,防止引用冲突) appData: JSON.parse(JSON.stringify(appData)) }; diff --git a/lib/design1/ui.js b/lib/design1/ui.js index c3d6027..3cb658e 100644 --- a/lib/design1/ui.js +++ b/lib/design1/ui.js @@ -917,6 +917,11 @@ $("#set_bg").click(function () {//设置为背�? // onStateRestored 函数在 core.js 中定义并附加到 window 对象 // ========================================== $("#previous").click(function () { + // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 + if (window._isApplyingHistory) { + return; + } + if (step.val == 0) return; step.val--; @@ -928,29 +933,50 @@ $("#previous").click(function () { objs = objs1; // 2. 恢复画布 (使用回调函数) + window._isApplyingHistory = true; canvas.loadFromJSON(recordJson1[step.val], function () { // 加载后解锁所有对象(背景除外) if (typeof window.unlockAllObjects === 'function') { window.unlockAllObjects(canvas); } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs1); + } if (typeof window.onStateRestored === 'function') { window.onStateRestored(); // 图片加载完才执行这一行 } + window._isApplyingHistory = false; }); } else { // 背面逻辑 objs2 = JSON.parse(recordObjs2[step.val]); objs = objs2; + window._isApplyingHistory = true; canvas.loadFromJSON(recordJson2[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs2); + } if (typeof window.onStateRestored === 'function') { window.onStateRestored(); } + window._isApplyingHistory = false; }); } }); // ========================================== $("#next").click(function () { + // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 + if (window._isApplyingHistory) { + return; + } + // 获取当前历史记录的最大长度 let maxLen = $("#front_side").hasClass("ui-button-active") ? recordObjs1.length : recordObjs2.length; @@ -962,23 +988,39 @@ $("#next").click(function () { objs1 = JSON.parse(recordObjs1[step.val]); objs = objs1; + window._isApplyingHistory = true; canvas.loadFromJSON(recordJson1[step.val], function () { // 加载后解锁所有对象(背景除外) if (typeof window.unlockAllObjects === 'function') { window.unlockAllObjects(canvas); } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs1); + } if (typeof window.onStateRestored === 'function') { window.onStateRestored(); // 图片加载完才执行这一行 } + window._isApplyingHistory = false; }); } else { objs2 = JSON.parse(recordObjs2[step.val]); objs = objs2; + window._isApplyingHistory = true; canvas.loadFromJSON(recordJson2[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs2); + } if (typeof window.onStateRestored === 'function') { window.onStateRestored(); } + window._isApplyingHistory = false; }); } }); @@ -1280,8 +1322,9 @@ $("#circle_stroke_transparent").click(function () { // saveAs 函数保留,因为它被作为回调传递给 output 函数(第52行) function saveAs(op1, callback) { - let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; + let j = canvas1.toJSON(props); + let b = canvas2.toJSON(props); // 清理不需要的 src 字段(使用 window.cleanupSrcFields,在 output.js 中定义) if (typeof window.cleanupSrcFields === 'function') { @@ -1634,7 +1677,8 @@ $("#circle_text_back_color").bind('input propertychange', function () { }) $("#circle_text_text").bind('input propertychange', function () { let val = $("#circle_text_text").val().trim() - canvas.getActiveObject().set("text", ` ${val} `); + // 有内容时不加首行缩进(前面的空格),只保留后面的空格 + canvas.getActiveObject().set("text", val ? `${val} ` : ` ${val} `); canvas.renderAll(); if (typeof window.updateList === 'function') { window.updateList(); diff --git a/lib/design2/core.js b/lib/design2/core.js index d39803b..8e711a1 100644 --- a/lib/design2/core.js +++ b/lib/design2/core.js @@ -1,2403 +1,2534 @@ -// design2核心功能模块 -// 注意:此代码在layui.use回调函数内部执行 -// 全局变量已在主入口文件中定义,这里只需要初始化 - -$('#canvas1').attr('height', document.body.clientHeight - 100) -$('#canvas2').attr('height', document.body.clientHeight - 100) -$('#object_attribute').css('height', '470px') -$('#obj_list').css('height', document.body.clientHeight - 520) -$('.obj_list').css('height', $('#obj_list').height() - 32) - -$('.main').show() -$('#canvas1').attr('width', $('#canvas-div').width()) -$('#canvas2').attr('width', $('#canvas-div').width()) - -// 初始化Canvas对象 -// 优化性能:在创建 Fabric Canvas 之前,先设置 canvas context 的 willReadFrequently -try { - const canvas1El = document.getElementById('canvas1'); - const canvas2El = document.getElementById('canvas2'); - if (canvas1El) { - canvas1El.getContext('2d', { willReadFrequently: true }); - } - if (canvas2El) { - canvas2El.getContext('2d', { willReadFrequently: true }); - } -} catch (e) { - // 如果浏览器不支持 willReadFrequently,忽略错误 -} - -canvas1 = new fabric.Canvas('canvas1') -canvas2 = new fabric.Canvas('canvas2') -// 同步到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 -window.canvas1 = canvas1; -window.canvas2 = canvas2; -fabric.Object.prototype.objectCaching = false -ctx1 = canvas1.getSelectionContext() -ctx2 = canvas2.getSelectionContext() -is_bgi_add = false -// 同步到 window 对象 -window.ctx1 = ctx1; -window.ctx2 = ctx2; -window.is_bgi_add = is_bgi_add; -canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) -//console.log(canvas1.width / 2, canvas1.height / 2); -//console.log($("#canvas-div").width() / 1200 > 1 ? 1 : $("#canvas-div").width() / 1200); -canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) -zoom = $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200 - -// 初始化标尺 -function initRulers() { - // dpi已在主入口文件中定义为全局变量 - const mmToPx = dpi / 25.4; // 1mm = 11.811px (300 DPI) - const rulerHeight = 20; - const rulerWidth = 20; - - // 获取实际画布的像素尺寸(px) - // design2.html 的实际画布尺寸是 1012px x 648px - const actualCanvasWidth = canvas1.width || 1012; // 实际画布宽度(px) - const actualCanvasHeight = canvas1.height || 648; // 实际画布高度(px) - - // 获取显示区域的尺寸(包括 margin) - const canvasDiv = $('#canvas-div'); - const canvasWrapper = $('.canvas-wrapper'); - // 使用 canvas-wrapper 的宽度,如果没有则使用 canvas_div 的宽度 - let canvasWidth = canvasWrapper.width(); - let canvasHeight = canvasWrapper.height(); - if (!canvasWidth || canvasWidth === 0) { - const canvas1Width = $('#canvas1').width() || canvasDiv.width() - 20; - canvasWidth = canvas1Width + 20; // 加上 margin-left: 20px - } - if (!canvasHeight || canvasHeight === 0) { - const canvas1Height = $('#canvas1').height() || canvasDiv.height() - 20; - canvasHeight = canvas1Height + 20; // 加上 margin-top: 20px - } - - // 设置标尺尺寸(使用显示区域的尺寸) - $('#ruler-h').attr('width', canvasWidth).attr('height', rulerHeight); - $('#ruler-v').attr('width', rulerWidth).attr('height', canvasHeight); - $('.ruler-horizontal').css('width', canvasWidth + 'px'); - $('.ruler-vertical').css('height', canvasHeight + 'px'); - - // 绘制水平标尺 - function drawHorizontalRuler() { - const ctx = document.getElementById('ruler-h').getContext('2d', { willReadFrequently: true }); - ctx.clearRect(0, 0, canvasWidth, rulerHeight); - - // 背景 - ctx.fillStyle = '#2C2C2C'; - ctx.fillRect(0, 0, canvasWidth, rulerHeight); - - // 边框 - ctx.strokeStyle = '#1E1E1E'; - ctx.lineWidth = 1; - ctx.strokeRect(0, 0, canvasWidth, rulerHeight); - ctx.beginPath(); - ctx.moveTo(0, rulerHeight - 1); - ctx.lineTo(canvasWidth, rulerHeight - 1); - ctx.stroke(); - - // 刻度线 - ctx.strokeStyle = '#888888'; - ctx.fillStyle = '#CCCCCC'; - ctx.font = '10px Arial'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - - // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 - // 实际画布物理宽度 = actualCanvasWidth / dpi * 25.4 (mm) - // 显示区域宽度 = actualCanvasWidth * zoom (px) - // 所以:1mm 在显示区域中 = (actualCanvasWidth * zoom) / (actualCanvasWidth / dpi * 25.4) = zoom * dpi / 25.4 - const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 - const marginOffset = 20; // canvas 的 margin-left: 20px - // 标尺0点应该在background_image的左上角位置 - // background_image在画布坐标系中的left值,需要转换到标尺坐标系 - // 画布从标尺的marginOffset位置开始,所以:标尺0点 = marginOffset + background_image.left (如果存在) - let originX = marginOffset; - if (typeof background_image !== 'undefined' && background_image) { - originX = marginOffset + background_image.left; - } - const startX = 0; - const endX = canvasWidth; - - // 绘制主要刻度(每10mm) - // 从0点向左绘制负值刻度 - for (let mm = -10; originX + mm * pxPerMm >= startX; mm -= 10) { - const x = originX + mm * pxPerMm; - if (x < startX) break; - - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, rulerHeight); - ctx.stroke(); - - // 数字(负值) - ctx.fillText(mm + 'mm', x + 2, 2); - } - // 从0点向右绘制正值刻度 - for (let mm = 0; originX + mm * pxPerMm <= endX; mm += 10) { - const x = originX + mm * pxPerMm; - if (x > endX) break; - - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, rulerHeight); - ctx.stroke(); - - // 数字 - ctx.fillText(mm + 'mm', x + 2, 2); - } - - // 绘制次要刻度(每1mm) - // 从0点向左绘制负值刻度 - for (let mm = -1; originX + mm * pxPerMm >= startX; mm -= 1) { - const x = originX + mm * pxPerMm; - if (x < startX) break; - if (mm % 10 === 0) continue; // 跳过主要刻度 - - ctx.beginPath(); - ctx.moveTo(x, rulerHeight - 8); - ctx.lineTo(x, rulerHeight); - ctx.stroke(); - } - // 从0点向右绘制正值刻度 - for (let mm = 0; originX + mm * pxPerMm <= endX; mm += 1) { - const x = originX + mm * pxPerMm; - if (x > endX) break; - if (mm % 10 === 0) continue; // 跳过主要刻度 - - ctx.beginPath(); - ctx.moveTo(x, rulerHeight - 8); - ctx.lineTo(x, rulerHeight); - ctx.stroke(); - } - } - - // 绘制垂直标尺 - function drawVerticalRuler() { - const ctx = document.getElementById('ruler-v').getContext('2d', { willReadFrequently: true }); - ctx.clearRect(0, 0, rulerWidth, canvasHeight); - - // 背景 - ctx.fillStyle = '#2C2C2C'; - ctx.fillRect(0, 0, rulerWidth, canvasHeight); - - // 边框 - ctx.strokeStyle = '#1E1E1E'; - ctx.lineWidth = 1; - ctx.strokeRect(0, 0, rulerWidth, canvasHeight); - ctx.beginPath(); - ctx.moveTo(rulerWidth - 1, 0); - ctx.lineTo(rulerWidth - 1, canvasHeight); - ctx.stroke(); - - // 刻度线 - ctx.strokeStyle = '#888888'; - ctx.fillStyle = '#CCCCCC'; - ctx.font = '10px Arial'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - - // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 - const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 - const marginOffset = 20; // canvas 的 margin-top: 20px (但还要加上ruler的高度) - const rulerHeightOffset = 20; // ruler 的高度 - // 标尺0点应该在background_image的左上角位置 - // background_image在画布坐标系中的top值,需要转换到标尺坐标系 - // 画布从标尺的marginOffset + rulerHeightOffset位置开始,所以:标尺0点 = marginOffset + rulerHeightOffset + background_image.top (如果存在) - let originY = marginOffset + rulerHeightOffset; - if (typeof background_image !== 'undefined' && background_image) { - originY = marginOffset + rulerHeightOffset + background_image.top; - } - const startY = 0; - const endY = canvasHeight; - - // 绘制主要刻度(每10mm) - // 从0点向上绘制负值刻度 - for (let mm = -10; originY + mm * pxPerMm >= startY; mm -= 10) { - const y = originY + mm * pxPerMm; - if (y < startY) break; - - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(rulerWidth, y); - ctx.stroke(); - - // 数字(旋转90度,负值) - ctx.save(); - ctx.translate(2, y + 8); - ctx.rotate(-Math.PI / 2); - ctx.fillText(mm + 'mm', 0, 0); - ctx.restore(); - } - // 从0点向下绘制正值刻度 - for (let mm = 0; originY + mm * pxPerMm <= endY; mm += 10) { - const y = originY + mm * pxPerMm; - if (y > endY) break; - - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(rulerWidth, y); - ctx.stroke(); - - // 数字(旋转90度) - ctx.save(); - ctx.translate(2, y + 8); - ctx.rotate(-Math.PI / 2); - ctx.fillText(mm + 'mm', 0, 0); - ctx.restore(); - } - - // 绘制次要刻度(每1mm) - // 从0点向上绘制负值刻度 - for (let mm = -1; originY + mm * pxPerMm >= startY; mm -= 1) { - const y = originY + mm * pxPerMm; - if (y < startY) break; - if (mm % 10 === 0) continue; // 跳过主要刻度 - - ctx.beginPath(); - ctx.moveTo(rulerWidth - 8, y); - ctx.lineTo(rulerWidth, y); - ctx.stroke(); - } - // 从0点向下绘制正值刻度 - for (let mm = 0; originY + mm * pxPerMm <= endY; mm += 1) { - const y = originY + mm * pxPerMm; - if (y > endY) break; - if (mm % 10 === 0) continue; // 跳过主要刻度 - - ctx.beginPath(); - ctx.moveTo(rulerWidth - 8, y); - ctx.lineTo(rulerWidth, y); - ctx.stroke(); - } - } - - // 初始绘制 - drawHorizontalRuler(); - drawVerticalRuler(); - - // 监听窗口大小变化 - $(window).on('resize', function () { - setTimeout(function () { - const canvasWrapper = $('.canvas-wrapper'); - let newWidth = canvasWrapper.width(); - let newHeight = canvasWrapper.height(); - if (!newWidth || newWidth === 0) { - const canvas1Width = $('#canvas1').width() || $('#canvas-div').width() - 20; - newWidth = canvas1Width + 20; // 加上 margin-left: 20px - } - if (!newHeight || newHeight === 0) { - const canvas1Height = $('#canvas1').height() || $('#canvas-div').height() - 20; - newHeight = canvas1Height + 20; // 加上 margin-top: 20px - } - if (newWidth !== canvasWidth || newHeight !== canvasHeight) { - canvasWidth = newWidth; - canvasHeight = newHeight; - $('#ruler-h').attr('width', newWidth); - $('#ruler-v').attr('height', newHeight); - $('.ruler-horizontal').css('width', newWidth + 'px'); - $('.ruler-vertical').css('height', newHeight + 'px'); - drawHorizontalRuler(); - drawVerticalRuler(); - } - }, 100); - }); - - // 监听画布缩放变化 - canvas1.on('after:render', function () { - drawHorizontalRuler(); - drawVerticalRuler(); - // 重新绘制辅助线(确保在遮罩之上) - const ctx = canvas1.getContext(); - const objects = canvas1.getObjects(); - objects.forEach(obj => { - if (obj.isGuideLine) { - ctx.save(); - const m = obj.calcTransformMatrix(); - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - ctx.globalAlpha = obj.opacity || 1; - if (obj.shadow) { - ctx.shadowColor = obj.shadow.color; - ctx.shadowBlur = obj.shadow.blur; - ctx.shadowOffsetX = obj.shadow.offsetX; - ctx.shadowOffsetY = obj.shadow.offsetY; - } - obj._render(ctx); - ctx.restore(); - } - }); - }); - canvas2.on('after:render', function () { - drawHorizontalRuler(); - drawVerticalRuler(); - // 重新绘制辅助线(确保在遮罩之上) - const ctx = canvas2.getContext(); - const objects = canvas2.getObjects(); - objects.forEach(obj => { - if (obj.isGuideLine) { - ctx.save(); - const m = obj.calcTransformMatrix(); - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - ctx.globalAlpha = obj.opacity || 1; - if (obj.shadow) { - ctx.shadowColor = obj.shadow.color; - ctx.shadowBlur = obj.shadow.blur; - ctx.shadowOffsetX = obj.shadow.offsetX; - ctx.shadowOffsetY = obj.shadow.offsetY; - } - obj._render(ctx); - ctx.restore(); - } - }); - }); - - // --- 辅助线功能 (参考 design1) --- - // canvasWrapper 已在上面声明,直接使用 - const canvasBox = $('#canvas-div'); // design2 使用 canvas-div 作为容器 - - // 确保容器定位 - if (canvasWrapper.css('position') === 'static') canvasWrapper.css('position', 'relative'); - - // 定义临时拖拽线 DOM - let $dragLine = null; - - // 通用拖拽开始函数 - const startDragGuide = (type, e) => { - e.preventDefault(); - e.stopPropagation(); - - // 创建临时显示的线 - $dragLine = $('
'); - canvasWrapper.append($dragLine); - - if (type === 'horizontal') { - // 从上往下拖 (水平线) - $dragLine.css({ - left: 0, width: '100%', height: '1px', top: e.offsetY - }); - } else { - // 从左往右拖 (垂直线) - $dragLine.css({ - top: 0, height: '100%', width: '1px', left: e.offsetX - }); - } - - // 绑定 document 移动事件 - $(document).on('mousemove.ruler', function (moveEvent) { - let wrapperOffset = canvasWrapper.offset(); - if (type === 'horizontal') { - let relY = moveEvent.pageY - wrapperOffset.top; - $dragLine.css('top', relY + 'px'); - } else { - let relX = moveEvent.pageX - wrapperOffset.left; - $dragLine.css('left', relX + 'px'); - } - }); - - // 绑定松开事件 (创建真实的 Fabric 线) - $(document).on('mouseup.ruler', function (upEvent) { - $(document).off('mousemove.ruler mouseup.ruler'); - - if (!$dragLine) return; - - // 获取最终位置 - let wrapperOffset = canvasWrapper.offset(); - let finalX = upEvent.pageX - wrapperOffset.left; - let finalY = upEvent.pageY - wrapperOffset.top; - - // 移除临时线 - $dragLine.remove(); - $dragLine = null; - - // 只有拖入画布区域才创建 - if (finalX > rulerWidth && finalY > rulerHeight) { - createFabricGuide(type, finalX, finalY); - } - }); - }; - - // 绑定到标尺 DOM - $('#ruler-h').off('mousedown').on('mousedown', (e) => startDragGuide('horizontal', e)); - $('#ruler-v').off('mousedown').on('mousedown', (e) => startDragGuide('vertical', e)); - - // --- 在画布中创建辅助线 --- - function createFabricGuide(type, domX, domY) { - // domX 和 domY 是相对于 canvas-wrapper 的坐标 - // 需要转换为相对于 canvas 的坐标,然后再转换为 Fabric 画布坐标 - - // 获取 canvas 相对于 canvas-wrapper 的位置 - let canvas1El = $('#canvas1'); - let canvasPosition = canvas1El.position(); - let canvasMarginLeft = parseFloat(canvas1El.css('margin-left')) || 0; - let canvasMarginTop = parseFloat(canvas1El.css('margin-top')) || 0; - - // 计算相对于 canvas 的坐标(减去 canvas 在 wrapper 中的位置和 margin) - let relX = domX - canvasPosition.left - canvasMarginLeft; - let relY = domY - canvasPosition.top - canvasMarginTop; - - // 转为 Fabric 坐标 (除以 zoom) - let canvasX = relX / zoom; - let canvasY = relY / zoom; - - // 使用足够大的固定值,确保辅助线能够覆盖整个画布和可见区域 - let lineExtent = 100000; - - // 构造线对象 - let linePoints = []; - if (type === 'horizontal') { - linePoints = [-lineExtent, canvasY, lineExtent, canvasY]; - } else { - linePoints = [canvasX, -lineExtent, canvasX, lineExtent]; - } - - let guideLine = new fabric.Line(linePoints, { - stroke: '#0066FF', - strokeWidth: 1 / zoom, - opacity: 0.9, - shadow: { - color: '#0066FF', - blur: 6, - offsetX: 0, - offsetY: 0, - affectStroke: true - }, - selectable: true, - evented: true, - hasControls: false, - hasBorders: false, - lockRotation: true, - lockScalingX: true, - lockScalingY: true, - hoverCursor: type === 'horizontal' ? 'ns-resize' : 'ew-resize', - perPixelTargetFind: false, - targetFindTolerance: 10, - padding: 10, - isGuideLine: true, - ignoreSave: true - }); - - // 限制移动方向 - guideLine.on('moving', function (e) { - if (type === 'horizontal') { - this.left = -lineExtent; - } else { - this.top = -lineExtent; - } - }); - - // 确定应该添加到哪个 canvas(使用当前活动的 canvas) - let targetCanvas = canvas; - if (!targetCanvas || (targetCanvas !== canvas1 && targetCanvas !== canvas2)) { - // 如果没有活动的 canvas,默认使用 canvas1 - targetCanvas = canvas1; - } - - targetCanvas.add(guideLine); - targetCanvas.setActiveObject(guideLine); - targetCanvas.renderAll(); - } - -} -initRulers(); -$('#source_front').width(zoom * $('#source_front').width()) -$('#source_back').width(zoom * $('#source_back').width()) -$('.canvas-container:eq(1)').hide() -canvas = canvas1 -window.canvas = canvas -fabric.Object.prototype.set({ - borderColor: '#9013FE', - cornerColor: '#9013FE', //激活状态角落图标的填充颜色 - cornerStrokeColor: '#9013FE', //激活状态角落图标的边框颜色 - borderOpacityWhenMoving: 1, - borderScaleFactor: 1, - cornerSize: 6, - cornerStyle: 'circle', //rect,circle - centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 - centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 - transparentCorners: false, //激活状态角落的图标是否透明 - rotatingPointOffset: 20, //旋转距旋转体的距离 - padding: 0 -}) -canvas1.preserveObjectStacking = true -canvas2.preserveObjectStacking = true - -// ========================================== -// 对象选择和属性更新函数 -// ========================================== -// 将 updateControls 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 -window.updateControls = function updateControls() { - // 使用 window.canvas 和 window.background_image 作为后备,确保在 eval() 执行时也能访问 - const currentCanvas = window.canvas || canvas; - const currentBackgroundImage = window.background_image || background_image; - if (!currentCanvas || !currentCanvas.getActiveObject()) return; - $("#text_r").val(currentCanvas.getActiveObject().get("angle").toFixed(0)); - $("#text_x").val((currentCanvas.getActiveObject().get("left") - currentBackgroundImage.left).toFixed(0)); - $("#text_y").val((currentCanvas.getActiveObject().get("top") - currentBackgroundImage.top).toFixed(0)); - $("#text_h").val((currentCanvas.getActiveObject().get("scaleY") * currentCanvas.getActiveObject().get("height")).toFixed(0)); - $("#text_w").val((currentCanvas.getActiveObject().get("scaleX") * currentCanvas.getActiveObject().get("width")).toFixed(0)); - // 根据锁定状态显示图标(使用lockMovementX判断,因为锁定时会设置所有lock属性) - if (currentCanvas.getActiveObject().get("lockMovementX")) { - $("#lock_img").attr("src", "public/images/lock20.png")//已锁定 - } else { - $("#lock_img").attr("src", "public/images/unlock20.png")//未锁定 - } -}; -// 向后兼容 -function updateControls() { - return window.updateControls(); -} -function selectObject(target, index) { - updateControls(); - $("#base_control").show(); -} - -function selectPic(target, index) { - // 检查是否有选中对象,如果没有则直接返回 - if (!canvas || !canvas.getActiveObject()) return; - - $("#pic_control").show(); - $("#set_bg").show(); - let activeObj = canvas.getActiveObject(); - - // 检查对象是否被锁定 - if (isObjectLocked(activeObj)) { - canvas.discardActiveObject(); - canvas.renderAll(); - return; - } - - if (typeof pic_op !== 'undefined' && pic_op && typeof pic_op.setValue === 'function') { - pic_op.setValue(100 - (activeObj.get("opacity") * 100)); - } else { - $("#pic_op").next().children(".layui-slider-bar").css("width", (1 - activeObj.get("opacity")) * 100 + "%"); - $("#pic_op").next().children(".layui-slider-handle").css("left", (1 - activeObj.get("opacity")) * 100 + "%"); - } - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); -} - -function selectOutPic(target, index) { - $("#out_pic_control").show(); - if (typeof out_pic_op !== 'undefined' && out_pic_op.setValue) { - out_pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); - } else { - $("#out_pic_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); - $("#out_pic_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); - } - let idx = getIndex(canvas.getActiveObject(), canvas); - $("#out_pic_field_name").val(objs[idx - 1].name); - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); -} - -function selectText(target, index) { - $("#text_control").show(); - $("#text_color").val(canvas.getActiveObject().get("fill")); - $("#text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); - let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi - if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 - $("#text_size").val(val); - $("#text_text").val(canvas.getActiveObject().get("text")); - $("#text_font_family").val(canvas.getActiveObject().get("fontFamily")); - - if (canvas.getActiveObject().get("underline") == true) { - $("#text_underline").addClass("ui-radio-acitve") - } else { - $("#text_underline").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontWeight") == "bold") { - $("#text_bold").addClass("ui-radio-acitve") - } else { - $("#text_bold").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontStyle") == "italic") { - $("#text_italics").addClass("ui-radio-acitve") - } else { - $("#text_italics").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("linethrough") == true) { - $("#text_linethrough").addClass("ui-radio-acitve") - } else { - $("#text_linethrough").removeClass("ui-radio-acitve") - } - $("#circle_text_control").hide(); - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); -} - -function selectCircleText(target, index) { - const activeObj = target || canvas.getActiveObject(); - if (!activeObj) return; - - let idx = getIndex(activeObj); - $("#circle_text_control").show(); - $("#circle_text_color").val(activeObj.get("fill")); - $("#circle_text_back_color").val(activeObj.get("textBackgroundColor") == "" ? '#ffffff' : activeObj.get("textBackgroundColor")); - - if (typeof circle_text_size !== 'undefined' && circle_text_size && typeof circle_text_size.setValue === 'function') { - if (typeof circle_text_size_updating !== 'undefined') { - circle_text_size_updating = true; - } - circle_text_size.setValue(activeObj.get("fontSize") - 10); - if (typeof circle_text_size_updating !== 'undefined') { - circle_text_size_updating = false; - } - } - - if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter && typeof circle_text_diameter.setValue === 'function') { - circle_text_diameter.setValue(activeObj.get("diameter") - 50); - } - - $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); - $("#circle_text_text").val(activeObj.get("text")); - $("#circle_text_font_family").val(activeObj.get("fontFamily")); - - if (activeObj.get("underline") == true) { - $("#circle_text_underline").addClass("ui-radio-acitve") - } else { - $("#circle_text_underline").removeClass("ui-radio-acitve") - } - if (activeObj.get("fontWeight") == "bold") { - $("#circle_text_bold").addClass("ui-radio-acitve") - } else { - $("#circle_text_bold").removeClass("ui-radio-acitve") - } - if (activeObj.get("fontStyle") == "italic") { - $("#circle_text_italics").addClass("ui-radio-acitve") - } else { - $("#circle_text_italics").removeClass("ui-radio-acitve") - } - if (activeObj.get("linethrough") == true) { - $("#circle_text_linethrough").addClass("ui-radio-acitve") - } else { - $("#circle_text_linethrough").removeClass("ui-radio-acitve") - } - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#rotate").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); -} - -function selectOutText(target, index) { - const align = canvas.getActiveObject().get('textAlign') - if (align === 'left') { - $('#out_left_alignment').parent().addClass('tool1') - } else if (align === 'right') { - $('#out_right_alignment').parent().addClass('tool1') - } else { - $('#out_center_alignment').parent().addClass('tool1') - } - $("#out_text_control").show(); - $("#out_text_color").val(canvas.getActiveObject().get("fill")); - $("#out_text_text").val(canvas.getActiveObject().get("text")); - let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi - if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 - $("#out_text_size").val(val); - $("#out_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); - let idx = getIndex(canvas.getActiveObject(), canvas); - $("#out_text_field_name").val(objs[idx - 1].name); - $("#out_text_font_family").val(canvas.getActiveObject().get("fontFamily")); - $("#exceed").prop("checked", objs[idx - 1].exceed ? 'checked' : ''); - $("#autoWrap").prop("checked", objs[idx - 1].autoWrap ? 'checked' : ''); - $("#fixSize").prop("checked", objs[idx - 1].fixSize ? 'checked' : ''); - if (canvas.getActiveObject().get("underline") == true) { - $("#out_text_underline").addClass("ui-radio-acitve") - } else { - $("#out_text_underline").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontWeight") == "bold") { - $("#out_text_bold").addClass("ui-radio-acitve") - } else { - $("#out_text_bold").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontStyle") == "italic") { - $("#out_text_italics").addClass("ui-radio-acitve") - } else { - $("#out_text_italics").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("linethrough") == true) { - $("#out_text_linethrough").addClass("ui-radio-acitve") - } else { - $("#out_text_linethrough").removeClass("ui-radio-acitve") - } - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); -} - -function selectRect(target, index) { - $("#rect_control").show(); - $("#rect_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); - $("#rect_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); - $("#rect_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); - $("#rect_radius").val(canvas.getActiveObject().get("rx")); - if (typeof rect_op !== 'undefined' && rect_op.setValue) { - rect_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); - } else { - $("#rect_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); - $("#rect_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); - } - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); -} - -function selectCircle(target, index) { - $("#circle_control").show(); - $("#circle_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); - $("#circle_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); - $("#circle_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); - if (typeof circle_op !== 'undefined' && circle_op.setValue) { - circle_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); - } else { - $("#circle_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); - $("#circle_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); - } - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); -} - -function selectLine(target, index) { - $("#line_control").show(); - let idx = getIndex(canvas.getActiveObject(), canvas); - $("#line_color").val(canvas.getActiveObject().get("stroke")); - $("#line_dist").val(canvas.getActiveObject().get("strokeDashArray")[0]); - $("#text_h").val(canvas.getActiveObject().get("strokeWidth")) - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); -} - -function selectQrCode(target, index) { - $("#qrcode_Controll").show(); - let idx = getIndex(canvas.getActiveObject(), canvas); - $("#qrcode_field_name").val(objs[idx - 1].name); - $("#qrcode_color").val(objs[idx - 1].color); - $("#qrcode_val").val(objs[idx - 1].val); - $("#line_control").hide(); - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#barcode_controll").hide(); -} - -function selectBarCode(target, index) { - $("#barcode_controll").show(); - let idx = getIndex(canvas.getActiveObject(), canvas); - $("#barcode_field_name").val(objs[idx - 1].name); - $("#barcode_font_family").val(objs[idx - 1].font); - let val = parseFloat(objs[idx - 1].fontSize) * 72 / dpi - if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 - $("#barcode_size").val(val); - $("#barcode_val").val(objs[idx - 1].val); - $("#barcode_show").prop("checked", objs[idx - 1].show ? 'checked' : ''); - $("#line_control").hide(); - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); -} - -function selectCounter(target, index) { - $("#counter_control").show(); - let idx = getIndex(canvas.getActiveObject(), canvas); - $("#counter_start").val(objs[idx - 1].start); - $("#counter_add").val(objs[idx - 1].add); - $("#counter_color").val(canvas.getActiveObject().get("fill")); - $("#count_font_family").val(canvas.getActiveObject().get("fontFamily")); - let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi - if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 - $("#count_size").val(val); - $("#line_control").hide(); - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); -} - -function handleObjectSelected(e) { - // 确定当前活动的 canvas(从事件参数或全局变量) - let currentCanvas = (e && e.target && (e.target.canvas === canvas1 || e.target.canvas === canvas2)) - ? (e.target.canvas === canvas1 ? canvas1 : canvas2) - : canvas; - - // 如果事件来自 canvas,更新全局 canvas 变量 - if (currentCanvas === canvas1 || currentCanvas === canvas2) { - canvas = currentCanvas; - objs = (currentCanvas === canvas1) ? objs1 : objs2; - window.canvas = canvas; - window.objs = objs; - } - - let select_obj = canvas.getActiveObject(); - - // 1. 未选中任何对象 - if (!select_obj) { - $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); - $("#component_type").text(language_str("bg")); - selectObj(-1, true); // 传 true,只更新UI - return; - } - - // 2. 如果是辅助线,直接返回,不处理(在获取索引之前检查) - if (select_obj.isGuideLine) { - return; - } - - // 3. 获取索引 - let i = getIndex(select_obj, canvas); - - // 如果不是通过对象列表选中的,且对象被锁定,则取消选中 - if (!window._selectingFromList && isObjectLocked(select_obj)) { - canvas.discardActiveObject(); - canvas.renderAll(); - $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); - $("#component_type").text(language_str("bg")); - selectObj(-1, true); - return; - } - - if (i != 0) { - // --- 选中了普通对象 --- - --i; // 转换为 objs 数组下标 - - // 安全检查:确保 objs[i] 存在且有 type 属性 - if (objs[i] && typeof objs[i].type !== 'undefined') { - switch (objs[i].type) { - case 1: // 图片 - $("#component_type").text(language_str("img")); - selectObject(select_obj, i); selectPic(select_obj, i); break; - case 2: // 合成图片 - $("#component_type").text(language_str("simg")); - selectObject(select_obj, i); selectOutPic(select_obj, i); break; - case 3: // 文本 - $("#component_type").text(language_str("text")); - selectObject(select_obj, i); selectText(select_obj, i); break; - case 4: // 合成文本 - $("#component_type").text(language_str("stext")); - selectObject(select_obj, i); selectOutText(select_obj, i); break; - case 5: // 矩形 - $("#component_type").text(language_str("rect")); - selectObject(select_obj, i); selectRect(select_obj, i); break; - case 6: // 圆形 - $("#component_type").text(language_str("circ")); - selectObject(select_obj, i); selectCircle(select_obj, i); break; - case 7: // 直线 - $("#component_type").text(language_str("line")); - selectObject(select_obj, i); selectLine(select_obj, i); break; - case 8: // 二维码 - $("#component_type").text(language_str("qrc")); - selectObject(select_obj, i); selectQrCode(select_obj, i); break; - case 9: // 条形码 - $("#component_type").text(language_str("barc")); - selectObject(select_obj, i); selectBarCode(select_obj, i); break; - case 10: // 计数器 - $("#component_type").text(language_str("counter")); - selectObject(select_obj, i); selectCounter(select_obj, i); break; - case 11: // 圆形文本 - $("#component_type").text(language_str("ctext")); - selectObject(select_obj, i); selectCircleText(select_obj, i); break; - } - } - - // 【关键】这里传入 true! - // 意思是:因为我已经是在画布上选中它了,selectObj 函数只负责把左边的条目变灰,不要再回头去 setActiveObject 了 - selectObj(i, true); - - } else { - // --- 选中了背景 --- - $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); - selectObj(-1, true); - $("#component_type").text(language_str("bg")); - } -} - -function handleObjectMoving(e) { - let obj = e.target; - // 跳过辅助线 - if (obj.isGuideLine) { - updateControls(); - return; - } - let idx = getIndex(obj, canvas); - // 圆形文本特殊处理 - // 安全检查:确保 objs[idx - 1] 存在且有 type 属性 - if (idx > 0 && idx <= objs.length && objs[idx - 1] && typeof objs[idx - 1].type !== 'undefined' && objs[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { - let pointer = { x: obj.left, y: obj.top }; - let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2); - let angleDeg = getDeg(pointer); - let flipped = pointer.y > canvas.height / 2 ? true : false; - - if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter.setValue) { - circle_text_diameter.setValue(distance * 2 - 50); - } - obj.set("diameter", distance * 2); - obj.set("angle", angleDeg); - obj.set("flipped", flipped); - } - updateControls(); -} - -// 辅助函数:检查对象是否被锁定 -// 只有当 lockMovementX 为 true 时才认为对象被锁定 -// selectable 为 false 可能是其他原因(如背景图),所以只检查 lockMovementX -function isObjectLocked(obj) { - if (!obj) return false; - // 主要检查 lockMovementX,这是锁定的主要标志 - return obj.get('lockMovementX') === true; -} -// 暴露到全局作用域,确保其他模块可以访问 -window.isObjectLocked = isObjectLocked; - -// 统一的 toJSON 属性列表,不包含锁定相关属性(锁定状态不应被保存) -const TO_JSON_PROPERTIES = [ - "selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline", "evented" -]; -// 暴露到全局作用域 -window.TO_JSON_PROPERTIES = TO_JSON_PROPERTIES; - -// 辅助函数:解锁所有对象(加载时使用) -function unlockAllObjects(canvas) { - if (!canvas) return; - const objects = canvas.getObjects(); - objects.forEach((obj, index) => { - // 背景对象(索引0)保持锁定状态 - if (index === 0) { - obj.set({ - lockMovementX: true, - lockMovementY: true, - lockRotation: true, - lockScalingX: true, - lockScalingY: true, - lockSkewingX: true, - lockSkewingY: true, - selectable: false, - evented: false - }); - } else { - // 其他对象默认解锁 - obj.set({ - lockMovementX: false, - lockMovementY: false, - lockRotation: false, - lockScalingX: false, - lockScalingY: false, - lockSkewingX: false, - lockSkewingY: false, - selectable: true, - evented: true - }); - } - }); -} -// 暴露到全局作用域 -window.unlockAllObjects = unlockAllObjects; - -// 将 handleObjectSelected 附加到 window 对象,确保全局可访问 -window.handleObjectSelected = handleObjectSelected; - -// 防止重复初始化事件监听器 -var _canvasEventsInitialized = false; - -function initCanvasEvents() { - // 如果已经初始化过,直接返回 - if (_canvasEventsInitialized) { - return; - } - _canvasEventsInitialized = true; - - [canvas1, canvas2].forEach(currentCanvas => { - // 1. 鼠标移动 - currentCanvas.on("mouse:move", (e) => { - let offsetX = 0, offsetY = 0; - if (currentCanvas === canvas2 && typeof background_image !== 'undefined') { - offsetX = background_image.left; - offsetY = background_image.top; - } - if (e.pointer) { - $("#posText").text("X: " + (e.pointer.x - offsetX).toFixed(0) + " Y: " + (e.pointer.y - offsetY).toFixed(0)); - } - }); - - // 2. 鼠标移出 - currentCanvas.on("mouse:out", () => { - // 可以在这里隐藏标尺辅助线 - }); - - // 3. 鼠标松开 (添加对象) - currentCanvas.on("mouse:up", (e) => { - if (addState === 0) return; - const ptr = e.pointer; - switch (addState) { - case 1: addPic(ptr); break; - case 2: addOutPic(ptr); break; - case 3: addText(ptr); break; - case 4: addOutText(ptr); break; - case 5: addRect(ptr); break; - case 6: addCircle(ptr); break; - case 7: addLine(ptr); break; - case 8: addQrCode(ptr); break; - case 9: addBarCode(ptr); break; - case 10: addCounter(ptr); break; - case 11: addCircleText(ptr); break; - } - addState = 0; - if (typeof background_image !== 'undefined') background_image.hoverCursor = "default"; - currentCanvas.renderAll(); - $("#addPic, #addOutPic, #addText, #addOutText, #addRect, #addCircle, #addLine, #addQrCode, #addBarCode, #addCounter, #addCircleText").css("background-color", ""); - }); - - // 4. 选中处理 - currentCanvas.on('mouse:down', (e) => { - // 更新全局 canvas 变量 - canvas = currentCanvas; - objs = (currentCanvas === canvas1) ? objs1 : objs2; - handleObjectSelected(e); - }); - // 阻止被锁定的对象被框选选中(只过滤被锁定的对象,保留未锁定的对象) - currentCanvas.on('before:selection:created', (e) => { - if (!e.targets || e.targets.length === 0) return; - // 过滤掉被锁定的对象,保留辅助线和未锁定的对象 - e.targets = e.targets.filter(target => { - if (target.isGuideLine) return true; // 保留辅助线 - return !isObjectLocked(target); // 移除被锁定的对象,保留未锁定的对象 - }); - }); - - // 添加 selection 事件以确保对象选中时正确显示属性面板 - currentCanvas.on('selection:created', (e) => { - canvas = currentCanvas; - objs = (currentCanvas === canvas1) ? objs1 : objs2; - handleObjectSelected(e); - }); - currentCanvas.on('selection:updated', (e) => { - canvas = currentCanvas; - objs = (currentCanvas === canvas1) ? objs1 : objs2; - handleObjectSelected(e); - }); - currentCanvas.on('object:selected', (e) => { - canvas = currentCanvas; - objs = (currentCanvas === canvas1) ? objs1 : objs2; - - // 如果不是通过对象列表选中的,且对象被锁定,则取消选中 - // 注意:只有真正被锁定的对象才会被取消选中,未锁定的对象应该正常选中 - if (!window._selectingFromList && e.target && isObjectLocked(e.target)) { - currentCanvas.discardActiveObject(); - currentCanvas.renderAll(); - return; - } - - handleObjectSelected(e); - // 更新对象列表以同步锁状态 - if (typeof window.updateList === 'function') { - window.updateList(); - } - }); - - // 5. 对象移动 - currentCanvas.on("object:moving", (e) => { - // 阻止背景图(索引0)被移动(如果已锁定) - let movingObj = e.target; - let idx = getIndex(movingObj, currentCanvas); - if (idx === 0) { - let bgObj = currentCanvas.getObjects()[0]; - if (bgObj && bgObj.get('lockMovementX') === true) { - e.e.preventDefault(); - e.e.stopPropagation(); - return false; - } - } - handleObjectMoving(e); - }); - - // 6. 变换事件 - currentCanvas.on("object:rotating", updateControls); - currentCanvas.on("object:scaling", updateControls); - currentCanvas.on("object:rotated", () => { recordState(); }); - currentCanvas.on('text:editing:entered', () => { _textEdit = true; }); - currentCanvas.on('text:editing:exited', () => { _textEdit = false; }); - currentCanvas.on("text:changed", (e) => { $("#text_text").val(e.target.get("text")); }); - - // 7. 对象修改后 - currentCanvas.on('object:modified', function (event) { - if (currentCanvas.getActiveObjects().length > 1) return; - // 跳过辅助线 - if (event.target.isGuideLine) { - return; - } - let idx = getIndex(event.target, currentCanvas); - if (idx <= 0) return; - let objData = objs[idx - 1]; - // 安全检查:确保 objData 存在且有 type 属性 - if (!objData || typeof objData.type === 'undefined') { - return; - } - let target = event.target; - - if ([3, 4, 10].includes(objData.type)) { - target.fontSize = (target.fontSize * target.scaleX).toFixed(0); - target.scaleX = 1; target.scaleY = 1; - target._clearCache(); - if (objData.type == 3) $("#text_size").val(target.fontSize); - if (objData.type == 4) $("#out_text_size").val(target.fontSize); - if (objData.type == 10) $("#count_size").val(target.fontSize); - currentCanvas.renderAll(); - updateControls(); - } else if (objData.type == 5) { - target.width *= target.scaleX; target.height *= target.scaleY; - target.scaleX = 1; target.scaleY = 1; - updateControls(); - } - recordState(); - // 更新对象列表以同步锁状态 - if (typeof window.updateList === 'function') { - window.updateList(); - } - }); - }); -} - -// 将 updateList 附加到 window 对象,确保全局可访问 -window.updateList = function updateList() { - let str = ""; - let i = 0; - - // 定义行样式:Flex布局,两端对齐,相对定位防止溢出 - const rowStyle = "display:flex; justify-content:space-between; align-items:center; height:32px; padding:0 5px; cursor:pointer; border-bottom:1px solid #444; color:#ccc; font-size:12px; position:relative;"; - // 定义左侧内容样式 - const leftStyle = "display:flex; align-items:center; overflow:hidden; white-space:nowrap; max-width:180px;"; - // 定义序号样式:靠右,有左边框 - const indexStyle = "min-width:24px; text-align:center; padding-left:5px; color:#666; border-left:1px solid #555;"; - - for (let item of objs) { - let displayIndex = i + 1; // 序号底层为1 - - // 图标逻辑 - let iconName = "rect.svg"; - switch (item.type) { - case 1: iconName = "images.svg"; break; - case 2: iconName = "images_add.svg"; break; - case 3: iconName = "text.svg"; break; - case 4: iconName = "text2.svg"; break; - case 5: iconName = "rect.svg"; break; - case 6: iconName = "round.svg"; break; - case 7: iconName = "line.svg"; break; - case 8: iconName = "qrcode.svg"; break; - case 9: iconName = "barcode.svg"; break; - case 10: iconName = "counter.svg"; break; - case 11: iconName = "text3.svg"; break; - } - - // 名称逻辑 - let nameText = item.name ? item.name : - (item.type === 3 ? "TEXT_" + displayIndex : - (item.type === 1 ? "IMG_" + displayIndex : "OBJ_" + displayIndex)); - - // 检查锁定状态(通过lockMovementX判断,因为锁定时会设置所有lock属性) - // 注意:索引0是背景图,objs数组是从下标0开始存第1个对象的,所以是 i+1 - let isLocked = false; - if (canvas.getObjects()[i + 1]) { - let canvasObj = canvas.getObjects()[i + 1]; - isLocked = canvasObj.get('lockMovementX') === true; - } - - // 锁定状态图标 - const lockIcon = isLocked ? - '' : - ''; - - // 组装 HTML - str = `
-
- - ${nameText} - ${lockIcon} -
- ${displayIndex} -
` + str; - i++; - } - $(".obj_list").html(str); - - // 重新绑定点击事件(防止 updateList 后点击失效) - $(".obj_list > div").on("click", function () { - let imgId = $(this).find("img").attr("id"); - if (imgId) { - let idx = parseInt(imgId.replace("obj", "")); - selectObj(idx); - } - }); -} - -// 标记是否通过对象列表选中(用于区分鼠标点击和列表点击) -window._selectingFromList = false; - -// 将 selectObj 附加到 window 对象,确保全局可访问 -window.selectObj = function selectObj(objIndices, isFromCanvas = false) { - let indicesArray = (Array.isArray(objIndices)) ? objIndices : ((typeof objIndices === 'number' && objIndices >= 0) ? [objIndices] : []); - - // 清除所有高亮 - $('.obj_list > div').css("background-color", "transparent"); - - // 遍历列表查找并高亮 - $('.obj_list > div').each(function () { - let $img = $(this).find("img"); - if ($img.length === 0) return; - - let idIndex = parseInt($img.attr("id").replace("obj", "")); - - if (indicesArray.includes(idIndex)) { - $(this).css("background-color", "#6F7A84"); - - // 自动滚动到可视区域 - if (isFromCanvas && indicesArray[0] === idIndex) { - this.scrollIntoView({ behavior: "smooth", block: "nearest" }); - } - } - }); - - // 如果是点击列表,触发画布选中 - if (!isFromCanvas && indicesArray.length === 1) { - let target = canvas.getObjects()[indicesArray[0] + 1]; - if (target) { - // 检查对象是否被锁定 - let isLocked = isObjectLocked(target); - - // 设置标志位,表示这是通过对象列表选中的(无论是否锁定) - window._selectingFromList = true; - - // 如果被锁定,临时设置 selectable 和 evented 为 true,以便能够选中 - if (isLocked) { - target.set({ - selectable: true, - evented: true - }); - } - - canvas.setActiveObject(target); - canvas.requestRenderAll(); - - // 如果对象被锁定,选中后立即恢复锁定状态(但保持选中状态) - if (isLocked) { - setTimeout(() => { - target.set({ - selectable: false, - evented: false - }); - canvas.requestRenderAll(); - }, 0); - } - - // 清除标志位(延迟清除,确保事件处理完成) - setTimeout(() => { - window._selectingFromList = false; - }, 10); - - handleObjectSelected(); - } - } -} - -// 将 recordState 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 -window.recordState = function recordState() { - // 使用 window 变量作为后备,确保在 eval() 执行时也能访问 - const currentStep = window.step || step; - const currentRecordObjs = window.recordObjs || recordObjs; - const currentRecordJson = window.recordJson || recordJson; - const currentObjs = window.objs || objs; - const currentCanvas = window.canvas || canvas; - ++currentStep.val; - for (let i = currentStep.val; i < currentRecordObjs.length; i++) { - currentRecordObjs.pop(); - currentRecordJson.pop(); - } - currentRecordObjs.push(JSON.stringify(currentObjs)) - // 保存所有锁定相关的属性,确保锁定状态可以正确恢复 - let j = currentCanvas.toJSON(TO_JSON_PROPERTIES); - j.objects[0].hoverCursor = "default"; - currentRecordJson.push(j); -}; -// 向后兼容 -function recordState() { - return window.recordState(); -} - -// 将 getIndex 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 -window.getIndex = function getIndex(target, _canvas = null) { - let temp_objs; - // 使用 window.canvas 作为默认值,确保在 eval() 执行时也能访问 - const currentCanvas = _canvas || window.canvas || canvas; - if (!currentCanvas) { - return 0; - } - temp_objs = currentCanvas.getObjects(); - - let i = 0; - for (let res of temp_objs) { - if (target == res) { - return i; - } - i++; - } - return 0; -}; -// 向后兼容 -function getIndex(target, _canvas = null) { - return window.getIndex(target, _canvas); -} - -function getType(target) { - // 如果是辅助线,返回 null - if (target.isGuideLine) { - return null; - } - let temp_objs = canvas.getObjects(); - let i = 0; - for (let res of temp_objs) { - if (target == res) { - // 安全检查:确保 objs[i] 存在且有 type 属性 - if (!objs[i] || typeof objs[i].type === 'undefined') { - return null; - } - return objs[i].type; - } - i++; - } - return null; -} - -function checkName(name) { - //检查字段名是否重复了 - for (let item of objs1) { - if (item.name == name) { - return false; - } - } - for (let item of objs2) { - if (item.name == name) { - return false; - } - } - return true; -} - -function resName(pre_name) { - //返回的是不重复的字段名 - let num = (objs1.length + objs2.length) - 1; - do { - num++; - } - while (!checkName(pre_name + num));//重复了就再加1 - return pre_name + num; -} - -function getDisdance(x1, y1, x2, y2) { - var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 - var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 - var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 - return distance; -} - -function getDeg(pointer) { - // 计算点击位置与画布中心的坐标差 - var centerX = canvas.width / 2; - var centerY = canvas.height / 2; - var mouseX = pointer.x; - var mouseY = pointer.y; - var dx = mouseX - centerX; - var dy = mouseY - centerY; - // 计算旋转角度(弧度) - var angleRad = Math.atan2(dy, dx); - // 将角度转换为度数 - var angleDeg = angleRad * (180 / Math.PI); - if (angleDeg < 0) { - angleDeg += 360; - } - angleDeg += 90 - return angleDeg -} - -function getCoordsMinX(acoords) { - x = acoords[0].x; - for (let item of acoords) { - if (item.x < x) { - x = item.x - } - } - return x; -} - -function getCoordsMaxX(acoords) { - x = acoords[0].x; - for (let item of acoords) { - if (item.x > x) { - x = item.x - } - } - return x; -} - -function getCoordsMinY(acoords) { - y = acoords[0].y; - for (let item of acoords) { - if (item.y < y) { - y = item.y - } - } - return y; -} - -function getCoordsMaxY(acoords) { - y = acoords[0].y; - for (let item of acoords) { - if (item.y > y) { - y = item.y - } - } - return y; -} - -function addBackground() { - // 类型改变 - fabric.Image.fromURL('./public/images/bg_front_2.png', function (image) { - image.left = ($('#canvas1').width() - image.width * image.get('scaleX')) / 2 - image.top = ($('#canvas1').height() - image.height * image.get('scaleY')) / 2 - - image.selectable = false - image.hoverable = false - image.hoverCursor = 'default' - image.setSrc(image.toDataURL(), function (image) { - background_image1 = image - background_image = background_image1 - window.background_image1 = background_image1; - window.background_image = background_image; - canvas1.add(image) - canvas1.renderAll() - - recordObjs1.push(JSON.stringify(objs1)) - recordJson1.push(canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline'])) - }) - // 类型改变 - fabric.Image.fromURL('./public/images/bg_back_2.png', function (image) { - image.left = ($('#canvas2').width() - image.width) / 2 - image.top = ($('#canvas2').height() - image.height) / 2 - - image.selectable = false - image.hoverable = false - image.hoverCursor = 'default' - - image.setSrc(image.toDataURL(), function (image) { - background_image2 = image - window.background_image2 = background_image2; - canvas2.add(image) - canvas2.renderAll() - - recordObjs2.push(JSON.stringify(objs2)) - recordJson2.push(canvas2.toJSON(TO_JSON_PROPERTIES)) - - let file = GetFile().get('file') - if (file && file != 'empty') { - window.openFile(file); - } - }) - }) - is_bgi_add = true - }) -} - -function addPic(pointer) { - pre_add_image.left = pointer.x - pre_add_image.top = pointer.y - - canvas.add(pre_add_image) - - objs.push({ type: 1, black: false }) - canvas.renderAll() - updateList() - recordState() -} - -function addOutPic(pointer) { - fabric.Image.fromURL('./public/images/outpic_extra.png', function (image) { - let width = canvas.getWidth() - let height = canvas.getHeight() - image.left = pointer.x - image.top = pointer.y - image.opacity = 1 - image.setSrc(image.toDataURL(), function (image) { - canvas.add(image) - objs.push({ type: 2, name: resName('OUT_PIC_'), black: false }) - canvas.renderAll() - updateList() - recordState() - }) - }) -} - -function addText(pointer) { - let text = new fabric.IText('TEXT', { - left: pointer.x, - top: pointer.y, - fill: '#000000', - textBackgroundColor: '', - fontStyle: 'normal', - fontWeight: 'normal', - underline: false, - linethrough: false, - fontFamily: 'Arial' - }) - text.setControlVisible('ml', false) - text.setControlVisible('mt', false) - text.setControlVisible('mr', false) - text.setControlVisible('mb', false) - canvas.add(text) - objs.push({ type: 3, black: false }) - canvas.renderAll() - updateList() - recordState() -} - -function addCircleText(pointer) { - let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2) - let angleDeg = getDeg(pointer) - let text = new fabric.CurvedText('Circle TEXT', { - left: pointer.x, - top: pointer.y, - fill: "#000000", - textBackgroundColor: "", - fontStyle: "normal", - fontWeight: "normal", - diameter: distance * 2, - fontSize: 30, - underline: false, - linethrough: false, - lockScalingX: true, - lockScalingY: true, - fontFamily: 'Arial', - originX: 'center', - originY: 'center', - angle: angleDeg, - flipped: pointer.y > canvas.height / 2 ? true : false - }); - text.setControlVisible("ml", false); - text.setControlVisible("mt", false); - text.setControlVisible("mr", false); - text.setControlVisible("mb", false); - canvas.add(text); - objs.push({ type: 11, black: false, circleCenter: 0 }); - canvas.renderAll(); - updateList(); - recordState(); -} - -function addOutText(pointer) { - let text = new fabric.Text('MERGE TEXT', { - left: pointer.x, - top: pointer.y, - fill: '#000000', - fontStyle: 'normal', - fontWeight: 'normal', - textBackgroundColor: '', - underline: false, - linethrough: false, - fontFamily: 'Arial', - textAlign: 'center' - }) - text.setControlVisible('ml', false) - text.setControlVisible('mt', false) - text.setControlVisible('mr', false) - text.setControlVisible('mb', false) - - canvas.add(text) - objs.push({ type: 4, name: resName('OUT_TEXT_'), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' }) - canvas.renderAll() - updateList() - recordState() -} - -function addRect(pointer) { - let rect = new fabric.Rect({ - left: pointer.x, - top: pointer.y, - fill: '', - stroke: "#ff0000", - strokeWidth: 3, - width: 100, - height: 100 - }) - canvas.add(rect) - objs.push({ type: 5, black: false }) - canvas.renderAll() - updateList() - recordState() -} - -function addCircle(pointer) { - let circle = new fabric.Circle({ - left: pointer.x, - top: pointer.y, - fill: '', - stroke: '#ff0000', - strokeWidth: 3, - radius: 100, - strokeUniform: true - }) - canvas.add(circle) - objs.push({ type: 6, black: false }) - canvas.renderAll() - updateList() - recordState() -} - -function addLine(pointer) { - let line = new fabric.Line([50, 100, 200, 100], { - left: pointer.x, - top: pointer.y, - stroke: '#ff0000', - strokeWidth: 3, - strokeDashArray: [0, 0], - }) - - line.setControlVisible('tl', false) - line.setControlVisible('tr', false) - line.setControlVisible('br', false) - line.setControlVisible('bl', false) - line.setControlVisible('mt', false) - line.setControlVisible('mb', false) - canvas.add(line) - objs.push({ type: 7, black: false }) - canvas.renderAll() - updateList() - recordState() -} - -function addQrCode(pointer) { - let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 }) - fabric.Image.fromURL(qr, function (image) { - image.left = pointer.x - image.top = pointer.y - canvas.add(image) - objs.push({ type: 8, name: resName('QR_CODE_'), color: '#000000', val: 'http://www.cardsoon.com', black: false }) - canvas.renderAll() - updateList() - recordState() - }) -} - -function addBarCode(pointer) { - JsBarcode('#barcode', '123456789', { margin: 0, lineColor: '#000000', fontSize: 18, font: 'Arial' }) - let barcode = document.getElementById('barcode') - let bar = barcode.toDataURL('image/png') - fabric.Image.fromURL(bar, function (image) { - image.left = pointer.x - image.top = pointer.y - - canvas.add(image) - objs.push({ type: 9, name: resName('BAR_CODE_'), val: '123456789', color: '#000000', fontSize: 18, font: 'Arial', show: false, black: false }) - canvas.renderAll() - updateList() - recordState() - }) -} - -function addCounter(pointer) { - let text = new fabric.Text('0', { - left: pointer.x, - top: pointer.y, - fill: '#000000' - }) - canvas.add(text) - objs.push({ type: 10, name: resName('COUNTER_'), start: 0, add: 1, black: false }) - canvas.renderAll() - updateList() - recordState() -} - -var front_list = [] -ipcRenderer.on('font-list', (event, data) => { - const newFontList = [] - data.map((item) => { - if (item.indexOf('"') === 0) { - newFontList.push(item.replace(/^"|"$/g, '')) - } else { - newFontList.push(item) - } - }) - front_list = newFontList - for (let item of front_list) { - $('#text_font_family').append("') - $("#circle_text_font_family").append(""); - $('#out_text_font_family').append("') - $('#barcode_font_family').append("') - $('#count_font_family').append("') - } -}) -ipcRenderer.on('sys-lan', (event, data) => { - let lang = localStorage.getItem('lang') - if (lang) { - s_lan = lang - // 同步到全局作用域(用于 .jsc 文件加载) - if (typeof window !== 'undefined') { - window.s_lan = s_lan; - } - if (typeof global !== 'undefined') { - global.s_lan = s_lan; - } - langua_ge(lang) - $('#language_select').val(lang) - return - } - switch (data) { - case 'zh-CN': - s_lan = 'zh' - // 同步到全局作用域(用于 .jsc 文件加载) - if (typeof window !== 'undefined') { - window.s_lan = s_lan; - } - if (typeof global !== 'undefined') { - global.s_lan = s_lan; - } - langua_ge('zh') - $('#language_select').val('zh') - localStorage.setItem('lang', 'zh') - break - case 'zh': - s_lan = 'zh' - // 同步到全局作用域(用于 .jsc 文件加载) - if (typeof window !== 'undefined') { - window.s_lan = s_lan; - } - if (typeof global !== 'undefined') { - global.s_lan = s_lan; - } - langua_ge('zh') - $('#language_select').val('zh') - localStorage.setItem('lang', 'zh') - break - case 'zh-TW': - s_lan = 'ozh' - // 同步到全局作用域(用于 .jsc 文件加载) - if (typeof window !== 'undefined') { - window.s_lan = s_lan; - } - if (typeof global !== 'undefined') { - global.s_lan = s_lan; - } - langua_ge('ozh') - $('#language_select').val('ozh') - localStorage.setItem('lang', 'ozh') - break - case 'en': - s_lan = 'en' - // 同步到全局作用域(用于 .jsc 文件加载) - if (typeof window !== 'undefined') { - window.s_lan = s_lan; - } - if (typeof global !== 'undefined') { - global.s_lan = s_lan; - } - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-AU': - s_lan = 'en' - // 同步到全局作用域(用于 .jsc 文件加载) - if (typeof window !== 'undefined') { - window.s_lan = s_lan; - } - if (typeof global !== 'undefined') { - global.s_lan = s_lan; - } - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-CA': - s_lan = 'en' - // 同步到全局作用域(用于 .jsc 文件加载) - if (typeof window !== 'undefined') { - window.s_lan = s_lan; - } - if (typeof global !== 'undefined') { - global.s_lan = s_lan; - } - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-GB': - s_lan = 'en' - // 同步到全局作用域(用于 .jsc 文件加载) - if (typeof window !== 'undefined') { - window.s_lan = s_lan; - } - if (typeof global !== 'undefined') { - global.s_lan = s_lan; - } - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-NZ': - s_lan = 'en' - // 同步到全局作用域(用于 .jsc 文件加载) - if (typeof window !== 'undefined') { - window.s_lan = s_lan; - } - if (typeof global !== 'undefined') { - global.s_lan = s_lan; - } - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-US': - s_lan = 'en' - // 同步到全局作用域(用于 .jsc 文件加载) - if (typeof window !== 'undefined') { - window.s_lan = s_lan; - } - if (typeof global !== 'undefined') { - global.s_lan = s_lan; - } - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-ZA': - s_lan = 'en' - // 同步到全局作用域(用于 .jsc 文件加载) - if (typeof window !== 'undefined') { - window.s_lan = s_lan; - } - if (typeof global !== 'undefined') { - global.s_lan = s_lan; - } - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - } -}) -ipcRenderer.on('close', (event, message) => { - layer.confirm( - '' + language_str('whetherSave') + '', - { - //是否保存当前文件? - btn: [language_str('save'), language_str('noSave'), language_str('cancel')], //['保存', '不保存', '取消'] - btn3: function (index, layero) { } - }, - function (index, layero) { - if (typeof window.output === 'function') { - window.output(() => ipcRenderer.send('run-close')); - } - }, - function (index) { - ipcRenderer.send('run-close') - } - ) -}) - -addBackground() //添加背景 白色卡片 - -initAligningGuidelines(canvas1) -initAligningGuidelines(canvas2) - -// 初始化 Canvas 事件监听 -initCanvasEvents(); - -// 初始化所有 Slider -var circle_text_size_updating = false; // 标志位,防止循环更新 -var circle_text_size = slider.render({ - elem: '#circle_text_size', - min: 10, - max: 120, - value: 50, - change: function (value) { - if (circle_text_size_updating) return; - if (canvas && canvas.getActiveObject && canvas.getActiveObject()) { - canvas.getActiveObject().set('fontSize', value); - canvas.renderAll(); - if (typeof updateControls === 'function') { - updateControls(); - } - if (typeof recordState === 'function') { - recordState(); - } - } - } -}); - -var circle_text_diameter = slider.render({ - elem: '#circle_text_diameter', - min: 50, - max: 1100, - value: 500, - change: function (value) { - if (canvas && canvas.getActiveObject && canvas.getActiveObject()) { - canvas.getActiveObject().set('diameter', value); - canvas.renderAll(); - if (typeof updateControls === 'function') { - updateControls(); - } - if (typeof recordState === 'function') { - recordState(); - } - } - } -}); - -var out_pic_op = slider.render({ - elem: '#out_pic_op', - min: 0, - max: 100, - value: 0, - change: function (value) { - if (canvas.getActiveObject()) { - canvas.getActiveObject().set('opacity', 1 - value / 100); - canvas.renderAll(); - updateControls(); - recordState(); - } - } -}); - -var pic_op = slider.render({ - elem: '#pic_op', - min: 0, - max: 100, - value: 0, - change: function (value) { - if (canvas.getActiveObject()) { - canvas.getActiveObject().set('opacity', 1 - value / 100); - canvas.renderAll(); - updateControls(); - recordState(); - } - } -}); - -var rect_op = slider.render({ - elem: '#rect_op', - min: 0, - max: 100, - value: 0, - change: function (value) { - if (canvas.getActiveObject()) { - canvas.getActiveObject().set('opacity', 1 - value / 100); - canvas.renderAll(); - updateControls(); - recordState(); - } - } -}); - -var circle_op = slider.render({ - elem: '#circle_op', - min: 0, - max: 100, - value: 0, - change: function (value) { - if (canvas.getActiveObject()) { - canvas.getActiveObject().set('opacity', 1 - value / 100); - canvas.renderAll(); - updateControls(); - recordState(); - } - } -}); - -//canvas.setZoom(0.5) -//recordObjs1.push(JSON.stringify(objs1)); -//recordJson1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); -//recordObjs2.push(JSON.stringify(objs2)); -//recordJson2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); -canvas.renderAll() //重新渲染画布 -var zoomSlider = slider.render({ - elem: '#zoomSlide', //绑定元素 - min: 25, - max: 500, - value: 100, - change(val) { - canvas.zoomToPoint(new fabric.Point(canvas.width / 2, canvas.height / 2), val / 100) - zoom = val / 100 - //$(".canvas-container").css("transform","scale(" + val / 100 + ")") - } -}) -window.rotate = function (id) { - let angle = $(`#${id}`).attr('angle') - 0 - angle += 90 - $(`#${id}`).css('transform', `rotate(${angle}deg)`) - $(`#${id}`).attr('angle', angle) -} -// 注意:#display 事件绑定已在 ui.js 中定义(第2633行),此处已删除重复定义 -// 导出文件 -// 将 savePdf 附加到 window 对象,确保在 output.js 中也能访问 -window.savePdf = async function savePdf(buffer) { - let fs = require('fs'); - // 弹出保存对话框 - const { canceled, filePath } = await dialog.showSaveDialog({ - title: '保存文件', - defaultPath: path.join(exePath, 'output.pdf'), // 默认路径 - filters: [{ name: 'PDF 文件', extensions: ['pdf'] }] // 文件过滤器 - }); - - if (canceled) { - return; - } - - // 将Buffer写入PDF文件到用户选择的路径 - fs.writeFile(filePath, buffer, (err) => { - if (err) { - } else { - layer.msg(language_str("saveSucc") + filePath);//'保存成功至' - } - }); -}; -// 向后兼容 -async function savePdf(buffer) { - return window.savePdf(buffer); -} - -// display_func 函数已移至 output.js,避免重复定义 -// function display_func(img1, img2, img3) { ... } - -// output 函数已移至 output.js,避免重复定义 -// function output(callback = null, _save = save) { ... } - -var _keyctrl = false, - _keyc = false, - _textEdit = false, - _inputEdit = false -$('input').focus(function () { - _inputEdit = true -}) -$('input').blur(function () { - _inputEdit = false -}) -$('textarea').focus(function () { - _inputEdit = true -}) -$('textarea').blur(function () { - _inputEdit = false -}) -$('body').keyup(function (event) { - // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) - if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释放 - _keyctrl = false - } - if (event.keyCode == 67) { - _keyc = false - } -}) -$('body').keydown(function (event) { - // 统一的快捷键处理(兼容 Windows/Linux: Ctrl, Mac: Cmd/Meta) - const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 - - // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的) - if (!_inputEdit && !_textEdit) { - // CTRL/CMD+N - 新建 - if (isModifierKey && event.keyCode == 78) { - event.preventDefault(); - $('#new').click(); - return; - } - // CTRL/CMD+O - 打开 - if (isModifierKey && event.keyCode == 79) { - event.preventDefault(); - $('#open').click(); - return; - } - // CTRL/CMD+S - 保存 - if (isModifierKey && !event.shiftKey && event.keyCode == 83) { - event.preventDefault(); - $('#save').click(); - return; - } - // CTRL/CMD+SHIFT+S - 保存副本 - if (isModifierKey && event.shiftKey && event.keyCode == 83) { - event.preventDefault(); - if (typeof window.output === 'function') { - window.output(null, saveAs); - } - return; - } - // CTRL/CMD+Z - 还原 - if (isModifierKey && !event.shiftKey && event.keyCode == 90) { - event.preventDefault(); - $('#previous').click(); - return; - } - // CTRL/CMD+Y - 重做 - if (isModifierKey && !event.shiftKey && event.keyCode == 89) { - event.preventDefault(); - $('#next').click(); - return; - } - // CTRL/CMD+C - 复制 - if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { - event.preventDefault(); - window.copySelection(); - return; - } - // CTRL/CMD+V - 粘贴 - if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { - event.preventDefault(); - window.pasteFromClipboard(); - return; - } - // CTRL/CMD+SHIFT+DEL - 清空 - if (isModifierKey && event.shiftKey && event.keyCode == 46) { - event.preventDefault(); - $('#clean').click(); - return; - } - // CTRL/CMD+DEL - 删除 - if (isModifierKey && !event.shiftKey && event.keyCode == 46) { - event.preventDefault(); - $('#delete').click(); - return; - } - } - // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) - if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd) - _keyctrl = true - if (_keyc && _textEdit) { - //同时按下且在编辑状态 - setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) - //clipboard.writeText((clipboard.readText()+' ')) - } - } - if (event.keyCode == 67) { - _keyc = true - if (_keyctrl & _textEdit) { - //同时按下且在编辑状态 - setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) - //clipboard.writeText((clipboard.readText()+' ')) - } - } - if (event.keyCode == 37) { - //left - let v = -1 - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = -5 - } - if (canvas.getActiveObject()) { - let x = canvas.getActiveObject().get('left') - x += v - canvas.getActiveObject().set('left', x) - canvas.renderAll() - recordState() - updateControls() - } - } - if (event.keyCode == 38) { - //up - let v = -1 - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = -5 - } - if (canvas.getActiveObject()) { - let y = canvas.getActiveObject().get('top') - y += v - canvas.getActiveObject().set('top', y) - canvas.renderAll() - recordState() - updateControls() - } - } - if (event.keyCode == 39) { - //right - let v = 1 - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = 5 - } - if (canvas.getActiveObject()) { - let x = canvas.getActiveObject().get('left') - x += v - canvas.getActiveObject().set('left', x) - canvas.renderAll() - recordState() - updateControls() - } - } - if (event.keyCode == 40) { - //down - let v = +1 - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = 5 - } - if (canvas.getActiveObject()) { - let y = canvas.getActiveObject().get('top') - y += v - canvas.getActiveObject().set('top', y) - canvas.renderAll() - recordState() - updateControls() - } - } - // 普通删除键(DEL键,没有按CTRL/CMD) - if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { - let actObjs = canvas.getActiveObjects() - let allObjs = canvas.getObjects() - let flag = 0 - for (let item of actObjs) { - if (item != allObjs[0]) { - let i = getIndex(item, canvas) //0是背景,所以从1开始,但objs不包含背景图片,故要-1 - objs.splice(i - 1, 1) - canvas.remove(item) - flag = 1 - } - } - updateList() - canvas.discardActiveObject() - if (flag == 1) { - recordState() - canvas.renderAll() - handleObjectSelected() - } - } -}) - -// 复制选中对象 -window.copySelection = function copySelection() { - const activeObjects = canvas.getActiveObjects(); - - if (!activeObjects || activeObjects.length === 0) { - layer.msg("未选中任何对象") - return; - } - - // 存储原始对象用于克隆 - clipboardData.objects = activeObjects; -} - -// 粘贴对象 -window.pasteFromClipboard = function pasteFromClipboard() { - if (!clipboardData.objects || clipboardData.objects.length === 0) { - layer.msg("剪贴板为空") - return; - } - - // 克隆所有对象 - fabric.util.enlivenObjects( - clipboardData.objects.map(obj => obj.toObject()), - clonedObjects => { - // 设置新位置(添加偏移) - clonedObjects.forEach((clone, index) => { - let idx = getIndex(clipboardData.objects[index], canvas) - clone.set({ - left: clone.left + clipboardData.offset, - top: clone.top + clipboardData.offset, - evented: true - }); - - // 添加到画布 - canvas.add(clone); - let tobj = { ...objs[idx - 1] } - if (tobj.name) tobj.name = tobj.name + objs.length - objs.push(tobj); - canvas.renderAll(); - updateList(); - recordState(); - }); - - // 选中粘贴的对象 - canvas.discardActiveObject(); - const sel = new fabric.ActiveSelection(clonedObjects, { - canvas: canvas - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - selectObj(objs.length - 1); - // 增加下次偏移 - clipboardData.offset += 10; - } - ); -} +// design2核心功能模块 +// 注意:此代码在layui.use回调函数内部执行 +// 全局变量已在主入口文件中定义,这里只需要初始化 + +$('#canvas1').attr('height', document.body.clientHeight - 100) +$('#canvas2').attr('height', document.body.clientHeight - 100) +$('#object_attribute').css('height', '470px') +$('#obj_list').css('height', document.body.clientHeight - 520) +$('.obj_list').css('height', $('#obj_list').height() - 32) + +$('.main').show() +$('#canvas1').attr('width', $('#canvas-div').width()) +$('#canvas2').attr('width', $('#canvas-div').width()) + +// 初始化Canvas对象 +// 优化性能:在创建 Fabric Canvas 之前,先设置 canvas context 的 willReadFrequently +try { + const canvas1El = document.getElementById('canvas1'); + const canvas2El = document.getElementById('canvas2'); + if (canvas1El) { + canvas1El.getContext('2d', { willReadFrequently: true }); + } + if (canvas2El) { + canvas2El.getContext('2d', { willReadFrequently: true }); + } +} catch (e) { + // 如果浏览器不支持 willReadFrequently,忽略错误 +} + +canvas1 = new fabric.Canvas('canvas1') +canvas2 = new fabric.Canvas('canvas2') +// 同步到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 +window.canvas1 = canvas1; +window.canvas2 = canvas2; +// 撤销/重做过程中使用的标志位,防止并发操作和误记录历史 +window._isApplyingHistory = false; +fabric.Object.prototype.objectCaching = false +ctx1 = canvas1.getSelectionContext() +ctx2 = canvas2.getSelectionContext() +is_bgi_add = false +// 同步到 window 对象 +window.ctx1 = ctx1; +window.ctx2 = ctx2; +window.is_bgi_add = is_bgi_add; +canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) +//console.log(canvas1.width / 2, canvas1.height / 2); +//console.log($("#canvas-div").width() / 1200 > 1 ? 1 : $("#canvas-div").width() / 1200); +canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) +zoom = $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200 + +// 初始化标尺 +function initRulers() { + // dpi已在主入口文件中定义为全局变量 + const mmToPx = dpi / 25.4; // 1mm = 11.811px (300 DPI) + const rulerHeight = 20; + const rulerWidth = 20; + + // 获取实际画布的像素尺寸(px) + // design2.html 的实际画布尺寸是 1012px x 648px + const actualCanvasWidth = canvas1.width || 1012; // 实际画布宽度(px) + const actualCanvasHeight = canvas1.height || 648; // 实际画布高度(px) + + // 获取显示区域的尺寸(包括 margin) + const canvasDiv = $('#canvas-div'); + const canvasWrapper = $('.canvas-wrapper'); + // 使用 canvas-wrapper 的宽度,如果没有则使用 canvas_div 的宽度 + let canvasWidth = canvasWrapper.width(); + let canvasHeight = canvasWrapper.height(); + if (!canvasWidth || canvasWidth === 0) { + const canvas1Width = $('#canvas1').width() || canvasDiv.width() - 20; + canvasWidth = canvas1Width + 20; // 加上 margin-left: 20px + } + if (!canvasHeight || canvasHeight === 0) { + const canvas1Height = $('#canvas1').height() || canvasDiv.height() - 20; + canvasHeight = canvas1Height + 20; // 加上 margin-top: 20px + } + + // 设置标尺尺寸(使用显示区域的尺寸) + $('#ruler-h').attr('width', canvasWidth).attr('height', rulerHeight); + $('#ruler-v').attr('width', rulerWidth).attr('height', canvasHeight); + $('.ruler-horizontal').css('width', canvasWidth + 'px'); + $('.ruler-vertical').css('height', canvasHeight + 'px'); + + // 绘制水平标尺 + function drawHorizontalRuler() { + const ctx = document.getElementById('ruler-h').getContext('2d', { willReadFrequently: true }); + ctx.clearRect(0, 0, canvasWidth, rulerHeight); + + // 背景 + ctx.fillStyle = '#2C2C2C'; + ctx.fillRect(0, 0, canvasWidth, rulerHeight); + + // 边框 + ctx.strokeStyle = '#1E1E1E'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, canvasWidth, rulerHeight); + ctx.beginPath(); + ctx.moveTo(0, rulerHeight - 1); + ctx.lineTo(canvasWidth, rulerHeight - 1); + ctx.stroke(); + + // 刻度线 + ctx.strokeStyle = '#888888'; + ctx.fillStyle = '#CCCCCC'; + ctx.font = '10px Arial'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 + // 实际画布物理宽度 = actualCanvasWidth / dpi * 25.4 (mm) + // 显示区域宽度 = actualCanvasWidth * zoom (px) + // 所以:1mm 在显示区域中 = (actualCanvasWidth * zoom) / (actualCanvasWidth / dpi * 25.4) = zoom * dpi / 25.4 + const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 + const marginOffset = 20; // canvas 的 margin-left: 20px + // 标尺0点应该在background_image的左上角位置 + // background_image在画布坐标系中的left值,需要转换到标尺坐标系 + // 画布从标尺的marginOffset位置开始,所以:标尺0点 = marginOffset + background_image.left (如果存在) + let originX = marginOffset; + if (typeof background_image !== 'undefined' && background_image) { + originX = marginOffset + background_image.left; + } + const startX = 0; + const endX = canvasWidth; + + // 绘制主要刻度(每10mm) + // 从0点向左绘制负值刻度 + for (let mm = -10; originX + mm * pxPerMm >= startX; mm -= 10) { + const x = originX + mm * pxPerMm; + if (x < startX) break; + + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + + // 数字(负值) + ctx.fillText(mm + 'mm', x + 2, 2); + } + // 从0点向右绘制正值刻度 + for (let mm = 0; originX + mm * pxPerMm <= endX; mm += 10) { + const x = originX + mm * pxPerMm; + if (x > endX) break; + + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + + // 数字 + ctx.fillText(mm + 'mm', x + 2, 2); + } + + // 绘制次要刻度(每1mm) + // 从0点向左绘制负值刻度 + for (let mm = -1; originX + mm * pxPerMm >= startX; mm -= 1) { + const x = originX + mm * pxPerMm; + if (x < startX) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(x, rulerHeight - 8); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + } + // 从0点向右绘制正值刻度 + for (let mm = 0; originX + mm * pxPerMm <= endX; mm += 1) { + const x = originX + mm * pxPerMm; + if (x > endX) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(x, rulerHeight - 8); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + } + } + + // 绘制垂直标尺 + function drawVerticalRuler() { + const ctx = document.getElementById('ruler-v').getContext('2d', { willReadFrequently: true }); + ctx.clearRect(0, 0, rulerWidth, canvasHeight); + + // 背景 + ctx.fillStyle = '#2C2C2C'; + ctx.fillRect(0, 0, rulerWidth, canvasHeight); + + // 边框 + ctx.strokeStyle = '#1E1E1E'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, rulerWidth, canvasHeight); + ctx.beginPath(); + ctx.moveTo(rulerWidth - 1, 0); + ctx.lineTo(rulerWidth - 1, canvasHeight); + ctx.stroke(); + + // 刻度线 + ctx.strokeStyle = '#888888'; + ctx.fillStyle = '#CCCCCC'; + ctx.font = '10px Arial'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 + const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 + const marginOffset = 20; // canvas 的 margin-top: 20px (但还要加上ruler的高度) + const rulerHeightOffset = 20; // ruler 的高度 + // 标尺0点应该在background_image的左上角位置 + // background_image在画布坐标系中的top值,需要转换到标尺坐标系 + // 画布从标尺的marginOffset + rulerHeightOffset位置开始,所以:标尺0点 = marginOffset + rulerHeightOffset + background_image.top (如果存在) + let originY = marginOffset + rulerHeightOffset; + if (typeof background_image !== 'undefined' && background_image) { + originY = marginOffset + rulerHeightOffset + background_image.top; + } + const startY = 0; + const endY = canvasHeight; + + // 绘制主要刻度(每10mm) + // 从0点向上绘制负值刻度 + for (let mm = -10; originY + mm * pxPerMm >= startY; mm -= 10) { + const y = originY + mm * pxPerMm; + if (y < startY) break; + + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + + // 数字(旋转90度,负值) + ctx.save(); + ctx.translate(2, y + 8); + ctx.rotate(-Math.PI / 2); + ctx.fillText(mm + 'mm', 0, 0); + ctx.restore(); + } + // 从0点向下绘制正值刻度 + for (let mm = 0; originY + mm * pxPerMm <= endY; mm += 10) { + const y = originY + mm * pxPerMm; + if (y > endY) break; + + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + + // 数字(旋转90度) + ctx.save(); + ctx.translate(2, y + 8); + ctx.rotate(-Math.PI / 2); + ctx.fillText(mm + 'mm', 0, 0); + ctx.restore(); + } + + // 绘制次要刻度(每1mm) + // 从0点向上绘制负值刻度 + for (let mm = -1; originY + mm * pxPerMm >= startY; mm -= 1) { + const y = originY + mm * pxPerMm; + if (y < startY) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(rulerWidth - 8, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + } + // 从0点向下绘制正值刻度 + for (let mm = 0; originY + mm * pxPerMm <= endY; mm += 1) { + const y = originY + mm * pxPerMm; + if (y > endY) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(rulerWidth - 8, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + } + } + + // 初始绘制 + drawHorizontalRuler(); + drawVerticalRuler(); + + // 监听窗口大小变化 + $(window).on('resize', function () { + setTimeout(function () { + const canvasWrapper = $('.canvas-wrapper'); + let newWidth = canvasWrapper.width(); + let newHeight = canvasWrapper.height(); + if (!newWidth || newWidth === 0) { + const canvas1Width = $('#canvas1').width() || $('#canvas-div').width() - 20; + newWidth = canvas1Width + 20; // 加上 margin-left: 20px + } + if (!newHeight || newHeight === 0) { + const canvas1Height = $('#canvas1').height() || $('#canvas-div').height() - 20; + newHeight = canvas1Height + 20; // 加上 margin-top: 20px + } + if (newWidth !== canvasWidth || newHeight !== canvasHeight) { + canvasWidth = newWidth; + canvasHeight = newHeight; + $('#ruler-h').attr('width', newWidth); + $('#ruler-v').attr('height', newHeight); + $('.ruler-horizontal').css('width', newWidth + 'px'); + $('.ruler-vertical').css('height', newHeight + 'px'); + drawHorizontalRuler(); + drawVerticalRuler(); + } + }, 100); + }); + + // 监听画布缩放/渲染变化,仅用于重绘标尺 + // 辅助线交给 Fabric 自己渲染,否则会出现“重影 / 变两三根”的现象 + canvas1.on('after:render', function () { + drawHorizontalRuler(); + drawVerticalRuler(); + }); + canvas2.on('after:render', function () { + drawHorizontalRuler(); + drawVerticalRuler(); + }); + + // --- 辅助线功能 (参考 design1) --- + // canvasWrapper 已在上面声明,直接使用 + const canvasBox = $('#canvas-div'); // design2 使用 canvas-div 作为容器 + + // 确保容器定位 + if (canvasWrapper.css('position') === 'static') canvasWrapper.css('position', 'relative'); + + // 定义临时拖拽线 DOM + let $dragLine = null; + + // 通用拖拽开始函数 + const startDragGuide = (type, e) => { + e.preventDefault(); + e.stopPropagation(); + + // 防止重复调用:如果正在拖拽,直接返回 + if ($dragLine && $dragLine.length > 0) { + return; + } + + // 创建临时显示的线 + $dragLine = $('
'); + canvasWrapper.append($dragLine); + + if (type === 'horizontal') { + // 从上往下拖 (水平线) + $dragLine.css({ + left: 0, width: '100%', height: '1px', top: e.offsetY + }); + } else { + // 从左往右拖 (垂直线) + $dragLine.css({ + top: 0, height: '100%', width: '1px', left: e.offsetX + }); + } + + // 绑定 document 移动事件 + $(document).on('mousemove.ruler', function (moveEvent) { + let wrapperOffset = canvasWrapper.offset(); + if (type === 'horizontal') { + let relY = moveEvent.pageY - wrapperOffset.top; + $dragLine.css('top', relY + 'px'); + } else { + let relX = moveEvent.pageX - wrapperOffset.left; + $dragLine.css('left', relX + 'px'); + } + }); + + // 绑定松开事件 (创建真实的 Fabric 线) + $(document).on('mouseup.ruler', function (upEvent) { + $(document).off('mousemove.ruler mouseup.ruler'); + + if (!$dragLine) return; + + // 获取最终位置 + let wrapperOffset = canvasWrapper.offset(); + let finalX = upEvent.pageX - wrapperOffset.left; + let finalY = upEvent.pageY - wrapperOffset.top; + + // 移除临时线 + $dragLine.remove(); + $dragLine = null; + + // 只有拖入画布区域才创建 + if (finalX > rulerWidth && finalY > rulerHeight) { + createFabricGuide(type, finalX, finalY); + } + }); + }; + + // 绑定到标尺 DOM + $('#ruler-h').off('mousedown').on('mousedown', (e) => startDragGuide('horizontal', e)); + $('#ruler-v').off('mousedown').on('mousedown', (e) => startDragGuide('vertical', e)); + + // --- 在画布中创建辅助线 --- + function createFabricGuide(type, domX, domY) { + // domX 和 domY 是相对于 canvas-wrapper 的坐标 + // 需要转换为相对于 canvas 的坐标,然后再转换为 Fabric 画布坐标 + + // 获取 canvas 相对于 canvas-wrapper 的位置 + let canvas1El = $('#canvas1'); + let canvasPosition = canvas1El.position(); + let canvasMarginLeft = parseFloat(canvas1El.css('margin-left')) || 0; + let canvasMarginTop = parseFloat(canvas1El.css('margin-top')) || 0; + + // 计算相对于 canvas 的坐标(减去 canvas 在 wrapper 中的位置和 margin) + let relX = domX - canvasPosition.left - canvasMarginLeft; + let relY = domY - canvasPosition.top - canvasMarginTop; + + // 转为 Fabric 坐标 (除以 zoom) + let canvasX = relX / zoom; + let canvasY = relY / zoom; + + // 使用与画布尺寸相关的长度,避免“整块区域都像辅助线” + let lineExtent = Math.max(canvas1.width, canvas1.height) * 2; + + // 构造线对象 + let linePoints = []; + if (type === 'horizontal') { + linePoints = [-lineExtent, canvasY, lineExtent, canvasY]; + } else { + linePoints = [canvasX, -lineExtent, canvasX, lineExtent]; + } + + let guideLine = new fabric.Line(linePoints, { + stroke: '#0066FF', + strokeWidth: 1 / zoom, + opacity: 0.9, + shadow: { + color: '#0066FF', + blur: 6, + offsetX: 0, + offsetY: 0, + affectStroke: true + }, + selectable: true, + evented: true, + hasControls: false, + hasBorders: false, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + hoverCursor: type === 'horizontal' ? 'ns-resize' : 'ew-resize', + // 只在靠近线条时触发命中,避免“大块区域都像辅助线” + // 同时适当放宽 tolerance,保证易于点中拖拽 + perPixelTargetFind: true, + targetFindTolerance: 6, + padding: 2, + isGuideLine: true, + ignoreSave: true + }); + + // 限制移动方向(只约束方向,不把整条线拖到极远,避免“整片区域是辅助线”的感觉) + guideLine.on('moving', function () { + if (type === 'horizontal') { + // 水平线:根据当前中心点的 Y,重算 x1/x2,使其横穿画布 + const cy = this.getCenterPoint().y; + this.set({ + x1: -lineExtent, + y1: cy, + x2: lineExtent, + y2: cy + }); + } else { + // 垂直线:根据当前中心点的 X,重算 y1/y2,使其竖穿画布 + const cx = this.getCenterPoint().x; + this.set({ + x1: cx, + y1: -lineExtent, + x2: cx, + y2: lineExtent + }); + } + }); + + // 确定应该添加到哪个 canvas(使用当前活动的 canvas) + let targetCanvas = canvas; + if (!targetCanvas || (targetCanvas !== canvas1 && targetCanvas !== canvas2)) { + // 如果没有活动的 canvas,默认使用 canvas1 + targetCanvas = canvas1; + } + + targetCanvas.add(guideLine); + targetCanvas.setActiveObject(guideLine); + targetCanvas.renderAll(); + } + +} +initRulers(); +$('#source_front').width(zoom * $('#source_front').width()) +$('#source_back').width(zoom * $('#source_back').width()) +$('.canvas-container:eq(1)').hide() +canvas = canvas1 +window.canvas = canvas +fabric.Object.prototype.set({ + borderColor: '#9013FE', + cornerColor: '#9013FE', //激活状态角落图标的填充颜色 + cornerStrokeColor: '#9013FE', //激活状态角落图标的边框颜色 + borderOpacityWhenMoving: 1, + borderScaleFactor: 1, + cornerSize: 6, + cornerStyle: 'circle', //rect,circle + centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 + centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 + transparentCorners: false, //激活状态角落的图标是否透明 + rotatingPointOffset: 20, //旋转距旋转体的距离 + padding: 0 +}) +canvas1.preserveObjectStacking = true +canvas2.preserveObjectStacking = true + +// ========================================== +// 对象选择和属性更新函数 +// ========================================== +// 将 updateControls 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 +window.updateControls = function updateControls() { + // 使用 window.canvas 和 window.background_image 作为后备,确保在 eval() 执行时也能访问 + const currentCanvas = window.canvas || canvas; + const currentBackgroundImage = window.background_image || background_image; + if (!currentCanvas || !currentCanvas.getActiveObject()) return; + $("#text_r").val(currentCanvas.getActiveObject().get("angle").toFixed(0)); + $("#text_x").val((currentCanvas.getActiveObject().get("left") - currentBackgroundImage.left).toFixed(0)); + $("#text_y").val((currentCanvas.getActiveObject().get("top") - currentBackgroundImage.top).toFixed(0)); + $("#text_h").val((currentCanvas.getActiveObject().get("scaleY") * currentCanvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((currentCanvas.getActiveObject().get("scaleX") * currentCanvas.getActiveObject().get("width")).toFixed(0)); + // 根据锁定状态显示图标(使用lockMovementX判断,因为锁定时会设置所有lock属性) + if (currentCanvas.getActiveObject().get("lockMovementX")) { + $("#lock_img").attr("src", "public/images/lock20.png")//已锁定 + } else { + $("#lock_img").attr("src", "public/images/unlock20.png")//未锁定 + } +}; +// 向后兼容 +function updateControls() { + return window.updateControls(); +} +function selectObject(target, index) { + updateControls(); + $("#base_control").show(); +} + +function selectPic(target, index) { + // 检查是否有选中对象,如果没有则直接返回 + if (!canvas || !canvas.getActiveObject()) return; + + $("#pic_control").show(); + $("#set_bg").show(); + let activeObj = canvas.getActiveObject(); + + // 检查对象是否被锁定: + // - 从画布点击时(_selectingFromList 为 false):不允许选中,被锁定的对象保持不可选状态 + // - 从对象列表点击时(_selectingFromList 为 true):允许选中,但对象在画布上仍然保持锁定 + if (!window._selectingFromList && isObjectLocked(activeObj)) { + canvas.discardActiveObject(); + canvas.renderAll(); + return; + } + + if (typeof pic_op !== 'undefined' && pic_op && typeof pic_op.setValue === 'function') { + pic_op.setValue(100 - (activeObj.get("opacity") * 100)); + } else { + $("#pic_op").next().children(".layui-slider-bar").css("width", (1 - activeObj.get("opacity")) * 100 + "%"); + $("#pic_op").next().children(".layui-slider-handle").css("left", (1 - activeObj.get("opacity")) * 100 + "%"); + } + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectOutPic(target, index) { + $("#out_pic_control").show(); + if (typeof out_pic_op !== 'undefined' && out_pic_op.setValue) { + out_pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + } else { + $("#out_pic_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + $("#out_pic_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + } + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#out_pic_field_name").val(objs[idx - 1].name); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectText(target, index) { + $("#text_control").show(); + $("#text_color").val(canvas.getActiveObject().get("fill")); + $("#text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#text_size").val(val); + $("#text_text").val(canvas.getActiveObject().get("text")); + $("#text_font_family").val(canvas.getActiveObject().get("fontFamily")); + + if (canvas.getActiveObject().get("underline") == true) { + $("#text_underline").addClass("ui-radio-acitve") + } else { + $("#text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#text_bold").addClass("ui-radio-acitve") + } else { + $("#text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#text_italics").addClass("ui-radio-acitve") + } else { + $("#text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#text_linethrough").addClass("ui-radio-acitve") + } else { + $("#text_linethrough").removeClass("ui-radio-acitve") + } + $("#circle_text_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectCircleText(target, index) { + const activeObj = target || canvas.getActiveObject(); + if (!activeObj) return; + + let idx = getIndex(activeObj); + $("#circle_text_control").show(); + $("#circle_text_color").val(activeObj.get("fill")); + $("#circle_text_back_color").val(activeObj.get("textBackgroundColor") == "" ? '#ffffff' : activeObj.get("textBackgroundColor")); + + if (typeof circle_text_size !== 'undefined' && circle_text_size && typeof circle_text_size.setValue === 'function') { + if (typeof circle_text_size_updating !== 'undefined') { + circle_text_size_updating = true; + } + circle_text_size.setValue(activeObj.get("fontSize") - 10); + if (typeof circle_text_size_updating !== 'undefined') { + circle_text_size_updating = false; + } + } + + if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter && typeof circle_text_diameter.setValue === 'function') { + circle_text_diameter.setValue(activeObj.get("diameter") - 50); + } + + $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); + $("#circle_text_text").val(activeObj.get("text")); + $("#circle_text_font_family").val(activeObj.get("fontFamily")); + + if (activeObj.get("underline") == true) { + $("#circle_text_underline").addClass("ui-radio-acitve") + } else { + $("#circle_text_underline").removeClass("ui-radio-acitve") + } + if (activeObj.get("fontWeight") == "bold") { + $("#circle_text_bold").addClass("ui-radio-acitve") + } else { + $("#circle_text_bold").removeClass("ui-radio-acitve") + } + if (activeObj.get("fontStyle") == "italic") { + $("#circle_text_italics").addClass("ui-radio-acitve") + } else { + $("#circle_text_italics").removeClass("ui-radio-acitve") + } + if (activeObj.get("linethrough") == true) { + $("#circle_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#circle_text_linethrough").removeClass("ui-radio-acitve") + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#rotate").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectOutText(target, index) { + const align = canvas.getActiveObject().get('textAlign') + if (align === 'left') { + $('#out_left_alignment').parent().addClass('tool1') + } else if (align === 'right') { + $('#out_right_alignment').parent().addClass('tool1') + } else { + $('#out_center_alignment').parent().addClass('tool1') + } + $("#out_text_control").show(); + $("#out_text_color").val(canvas.getActiveObject().get("fill")); + $("#out_text_text").val(canvas.getActiveObject().get("text")); + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#out_text_size").val(val); + $("#out_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#out_text_field_name").val(objs[idx - 1].name); + $("#out_text_font_family").val(canvas.getActiveObject().get("fontFamily")); + $("#exceed").prop("checked", objs[idx - 1].exceed ? 'checked' : ''); + $("#autoWrap").prop("checked", objs[idx - 1].autoWrap ? 'checked' : ''); + $("#fixSize").prop("checked", objs[idx - 1].fixSize ? 'checked' : ''); + if (canvas.getActiveObject().get("underline") == true) { + $("#out_text_underline").addClass("ui-radio-acitve") + } else { + $("#out_text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#out_text_bold").addClass("ui-radio-acitve") + } else { + $("#out_text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#out_text_italics").addClass("ui-radio-acitve") + } else { + $("#out_text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#out_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#out_text_linethrough").removeClass("ui-radio-acitve") + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectRect(target, index) { + $("#rect_control").show(); + $("#rect_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); + $("#rect_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); + $("#rect_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); + $("#rect_radius").val(canvas.getActiveObject().get("rx")); + if (typeof rect_op !== 'undefined' && rect_op.setValue) { + rect_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + } else { + $("#rect_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + $("#rect_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectCircle(target, index) { + $("#circle_control").show(); + $("#circle_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); + $("#circle_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); + $("#circle_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); + if (typeof circle_op !== 'undefined' && circle_op.setValue) { + circle_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + } else { + $("#circle_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + $("#circle_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectLine(target, index) { + $("#line_control").show(); + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#line_color").val(canvas.getActiveObject().get("stroke")); + $("#line_dist").val(canvas.getActiveObject().get("strokeDashArray")[0]); + $("#text_h").val(canvas.getActiveObject().get("strokeWidth")) + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectQrCode(target, index) { + $("#qrcode_Controll").show(); + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#qrcode_field_name").val(objs[idx - 1].name); + $("#qrcode_color").val(objs[idx - 1].color); + $("#qrcode_val").val(objs[idx - 1].val); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#barcode_controll").hide(); +} + +function selectBarCode(target, index) { + $("#barcode_controll").show(); + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#barcode_field_name").val(objs[idx - 1].name); + $("#barcode_font_family").val(objs[idx - 1].font); + let val = parseFloat(objs[idx - 1].fontSize) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#barcode_size").val(val); + $("#barcode_val").val(objs[idx - 1].val); + $("#barcode_show").prop("checked", objs[idx - 1].show ? 'checked' : ''); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); +} + +function selectCounter(target, index) { + $("#counter_control").show(); + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#counter_start").val(objs[idx - 1].start); + $("#counter_add").val(objs[idx - 1].add); + $("#counter_color").val(canvas.getActiveObject().get("fill")); + $("#count_font_family").val(canvas.getActiveObject().get("fontFamily")); + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#count_size").val(val); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function handleObjectSelected(e) { + // 确定当前活动的 canvas(从事件参数或全局变量) + let currentCanvas = (e && e.target && (e.target.canvas === canvas1 || e.target.canvas === canvas2)) + ? (e.target.canvas === canvas1 ? canvas1 : canvas2) + : canvas; + + // 如果事件来自 canvas,更新全局 canvas 变量 + if (currentCanvas === canvas1 || currentCanvas === canvas2) { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + window.canvas = canvas; + window.objs = objs; + } + + let select_obj = canvas.getActiveObject(); + + // 1. 未选中任何对象 + if (!select_obj) { + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + $("#component_type").text(language_str("bg")); + selectObj(-1, true); // 传 true,只更新UI + return; + } + + // 2. 如果是辅助线,直接返回,不处理(在获取索引之前检查) + if (select_obj.isGuideLine) { + return; + } + + // 3. 获取索引 + let i = getIndex(select_obj, canvas); + + // 如果不是通过对象列表选中的,且对象被锁定,则取消选中 + if (!window._selectingFromList && isObjectLocked(select_obj)) { + canvas.discardActiveObject(); + canvas.renderAll(); + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + $("#component_type").text(language_str("bg")); + selectObj(-1, true); + return; + } + + if (i != 0) { + // --- 选中了普通对象 --- + --i; // 转换为 objs 数组下标 + + // 安全检查:确保 objs[i] 存在且有 type 属性 + if (objs[i] && typeof objs[i].type !== 'undefined') { + switch (objs[i].type) { + case 1: // 图片 + $("#component_type").text(language_str("img")); + selectObject(select_obj, i); selectPic(select_obj, i); break; + case 2: // 合成图片 + $("#component_type").text(language_str("simg")); + selectObject(select_obj, i); selectOutPic(select_obj, i); break; + case 3: // 文本 + $("#component_type").text(language_str("text")); + selectObject(select_obj, i); selectText(select_obj, i); break; + case 4: // 合成文本 + $("#component_type").text(language_str("stext")); + selectObject(select_obj, i); selectOutText(select_obj, i); break; + case 5: // 矩形 + $("#component_type").text(language_str("rect")); + selectObject(select_obj, i); selectRect(select_obj, i); break; + case 6: // 圆形 + $("#component_type").text(language_str("circ")); + selectObject(select_obj, i); selectCircle(select_obj, i); break; + case 7: // 直线 + $("#component_type").text(language_str("line")); + selectObject(select_obj, i); selectLine(select_obj, i); break; + case 8: // 二维码 + $("#component_type").text(language_str("qrc")); + selectObject(select_obj, i); selectQrCode(select_obj, i); break; + case 9: // 条形码 + $("#component_type").text(language_str("barc")); + selectObject(select_obj, i); selectBarCode(select_obj, i); break; + case 10: // 计数器 + $("#component_type").text(language_str("counter")); + selectObject(select_obj, i); selectCounter(select_obj, i); break; + case 11: // 圆形文本 + $("#component_type").text(language_str("ctext")); + selectObject(select_obj, i); selectCircleText(select_obj, i); break; + } + } + + // 【关键】这里传入 true! + // 意思是:因为我已经是在画布上选中它了,selectObj 函数只负责把左边的条目变灰,不要再回头去 setActiveObject 了 + selectObj(i, true); + + } else { + // --- 选中了背景 --- + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + selectObj(-1, true); + $("#component_type").text(language_str("bg")); + } +} + +function handleObjectMoving(e) { + let obj = e.target; + // 跳过辅助线 + if (obj.isGuideLine) { + updateControls(); + return; + } + let idx = getIndex(obj, canvas); + // 圆形文本特殊处理 + // 安全检查:确保 objs[idx - 1] 存在且有 type 属性 + if (idx > 0 && idx <= objs.length && objs[idx - 1] && typeof objs[idx - 1].type !== 'undefined' && objs[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { + let pointer = { x: obj.left, y: obj.top }; + let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2); + let angleDeg = getDeg(pointer); + let flipped = pointer.y > canvas.height / 2 ? true : false; + + if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter.setValue) { + circle_text_diameter.setValue(distance * 2 - 50); + } + obj.set("diameter", distance * 2); + obj.set("angle", angleDeg); + obj.set("flipped", flipped); + } + updateControls(); +} + +// 辅助函数:检查对象是否被锁定 +// 只有当 lockMovementX 为 true 时才认为对象被锁定 +// selectable 为 false 可能是其他原因(如背景图),所以只检查 lockMovementX +function isObjectLocked(obj) { + if (!obj) return false; + // 主要检查 lockMovementX,这是锁定的主要标志 + return obj.get('lockMovementX') === true; +} +// 暴露到全局作用域,确保其他模块可以访问 +window.isObjectLocked = isObjectLocked; + +// 统一的 toJSON 属性列表,不包含锁定相关属性(锁定状态不应被保存) +// 包含所有需要保存的属性,特别是自定义对象的属性(如 CurvedText 的 diameter, flipped) +const TO_JSON_PROPERTIES = [ + "selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline", + "evented", "linethrough", "textBackgroundColor", "diameter", "flipped" +]; +// 暴露到全局作用域 +window.TO_JSON_PROPERTIES = TO_JSON_PROPERTIES; + +// 辅助函数:解锁所有对象(加载时使用) +function unlockAllObjects(canvas) { + if (!canvas) return; + const objects = canvas.getObjects(); + objects.forEach((obj, index) => { + // 背景对象(索引0)保持锁定状态 + if (index === 0) { + obj.set({ + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + lockSkewingX: true, + lockSkewingY: true, + selectable: false, + evented: false + }); + } else { + // 其他对象默认解锁 + obj.set({ + lockMovementX: false, + lockMovementY: false, + lockRotation: false, + lockScalingX: false, + lockScalingY: false, + lockSkewingX: false, + lockSkewingY: false, + selectable: true, + evented: true + }); + } + }); +} +// 暴露到全局作用域 +window.unlockAllObjects = unlockAllObjects; + +// 将 handleObjectSelected 附加到 window 对象,确保全局可访问 +window.handleObjectSelected = handleObjectSelected; + +// 防止重复初始化事件监听器 +var _canvasEventsInitialized = false; + +function initCanvasEvents() { + // 如果已经初始化过,直接返回 + if (_canvasEventsInitialized) { + return; + } + _canvasEventsInitialized = true; + + [canvas1, canvas2].forEach(currentCanvas => { + // 1. 鼠标移动 + currentCanvas.on("mouse:move", (e) => { + let offsetX = 0, offsetY = 0; + if (currentCanvas === canvas2 && typeof background_image !== 'undefined') { + offsetX = background_image.left; + offsetY = background_image.top; + } + if (e.pointer) { + $("#posText").text("X: " + (e.pointer.x - offsetX).toFixed(0) + " Y: " + (e.pointer.y - offsetY).toFixed(0)); + } + }); + + // 2. 鼠标移出 + currentCanvas.on("mouse:out", () => { + // 可以在这里隐藏标尺辅助线 + }); + + // 3. 鼠标松开 (添加对象) + currentCanvas.on("mouse:up", (e) => { + if (addState === 0) return; + const ptr = e.pointer; + switch (addState) { + case 1: addPic(ptr); break; + case 2: addOutPic(ptr); break; + case 3: addText(ptr); break; + case 4: addOutText(ptr); break; + case 5: addRect(ptr); break; + case 6: addCircle(ptr); break; + case 7: addLine(ptr); break; + case 8: addQrCode(ptr); break; + case 9: addBarCode(ptr); break; + case 10: addCounter(ptr); break; + case 11: addCircleText(ptr); break; + } + addState = 0; + if (typeof background_image !== 'undefined') background_image.hoverCursor = "default"; + currentCanvas.renderAll(); + $("#addPic, #addOutPic, #addText, #addOutText, #addRect, #addCircle, #addLine, #addQrCode, #addBarCode, #addCounter, #addCircleText").css("background-color", ""); + }); + + // 4. 选中处理 + currentCanvas.on('mouse:down', (e) => { + // 更新全局 canvas 变量 + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + handleObjectSelected(e); + }); + // 阻止被锁定的对象被框选选中,同时彻底排除辅助线 + currentCanvas.on('before:selection:created', (e) => { + if (!e.targets || e.targets.length === 0) return; + + // 调试日志:原始选区 + console.log('[Guide] before:selection:created raw targets', e.targets.map(t => ({ + isGuideLine: !!t.isGuideLine, + type: t.type, + left: t.left, + top: t.top + }))); + + // 选区里彻底排除辅助线:不允许辅助线进入 ActiveSelection + // 只保留未锁定的普通对象 + e.targets = e.targets.filter(target => { + if (target.isGuideLine) return false; // 完全移除辅助线 + return !isObjectLocked(target); // 过滤掉被锁定的普通对象 + }); + + // 调试日志:过滤后的选区 + console.log('[Guide] before:selection:created filtered targets', e.targets.map(t => ({ + isGuideLine: !!t.isGuideLine, + type: t.type, + left: t.left, + top: t.top + }))); + }); + + // 添加 selection 事件以确保对象选中时正确显示属性面板 + // 注意:真正排除辅助线的逻辑已经在 before:selection:created 里完成, + // 这里不再对 ActiveSelection 做任何重建,避免 Fabric 内部递归导致栈溢出。 + currentCanvas.on('selection:created', (e) => { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + handleObjectSelected(e); + }); + currentCanvas.on('selection:updated', (e) => { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + handleObjectSelected(e); + }); + currentCanvas.on('object:selected', (e) => { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + + // 如果不是通过对象列表选中的,且对象被锁定,则取消选中 + // 注意:只有真正被锁定的对象才会被取消选中,未锁定的对象应该正常选中 + if (!window._selectingFromList && e.target && isObjectLocked(e.target)) { + currentCanvas.discardActiveObject(); + currentCanvas.renderAll(); + return; + } + + handleObjectSelected(e); + // 更新对象列表以同步锁状态 + if (typeof window.updateList === 'function') { + window.updateList(); + } + }); + + // 5. 对象移动 + currentCanvas.on("object:moving", (e) => { + // 阻止背景图(索引0)被移动(如果已锁定) + let movingObj = e.target; + let idx = getIndex(movingObj, currentCanvas); + if (idx === 0) { + let bgObj = currentCanvas.getObjects()[0]; + if (bgObj && bgObj.get('lockMovementX') === true) { + e.e.preventDefault(); + e.e.stopPropagation(); + return false; + } + } + handleObjectMoving(e); + }); + + // 6. 变换事件 + currentCanvas.on("object:rotating", updateControls); + currentCanvas.on("object:scaling", updateControls); + currentCanvas.on("object:rotated", () => { recordState(); }); + currentCanvas.on('text:editing:entered', () => { _textEdit = true; }); + currentCanvas.on('text:editing:exited', () => { _textEdit = false; }); + currentCanvas.on("text:changed", (e) => { $("#text_text").val(e.target.get("text")); }); + + // 7. 对象修改后 + currentCanvas.on('object:modified', function (event) { + if (currentCanvas.getActiveObjects().length > 1) return; + // 跳过辅助线 + if (event.target.isGuideLine) { + return; + } + let idx = getIndex(event.target, currentCanvas); + if (idx <= 0) return; + let objData = objs[idx - 1]; + // 安全检查:确保 objData 存在且有 type 属性 + if (!objData || typeof objData.type === 'undefined') { + return; + } + let target = event.target; + + if ([3, 4, 10].includes(objData.type)) { + target.fontSize = (target.fontSize * target.scaleX).toFixed(0); + target.scaleX = 1; target.scaleY = 1; + target._clearCache(); + if (objData.type == 3) $("#text_size").val(target.fontSize); + if (objData.type == 4) $("#out_text_size").val(target.fontSize); + if (objData.type == 10) $("#count_size").val(target.fontSize); + currentCanvas.renderAll(); + updateControls(); + } else if (objData.type == 5) { + target.width *= target.scaleX; target.height *= target.scaleY; + target.scaleX = 1; target.scaleY = 1; + updateControls(); + } + recordState(); + // 更新对象列表以同步锁状态 + if (typeof window.updateList === 'function') { + window.updateList(); + } + }); + }); +} + +// 将 updateList 附加到 window 对象,确保全局可访问 +window.updateList = function updateList() { + // 确保使用正确的 objs(优先使用 window.objs,因为它可能被撤回/重做更新) + const currentObjs = window.objs || objs; + const currentCanvas = window.canvas || canvas; + + let str = ""; + let i = 0; + + // 定义行样式:Flex布局,两端对齐,相对定位防止溢出 + const rowStyle = "display:flex; justify-content:space-between; align-items:center; height:32px; padding:0 5px; cursor:pointer; border-bottom:1px solid #444; color:#ccc; font-size:12px; position:relative;"; + // 定义左侧内容样式 + const leftStyle = "display:flex; align-items:center; overflow:hidden; white-space:nowrap; max-width:180px;"; + // 定义序号样式:靠右,有左边框 + const indexStyle = "min-width:24px; text-align:center; padding-left:5px; color:#666; border-left:1px solid #555;"; + + for (let item of currentObjs) { + let displayIndex = i + 1; // 序号底层为1 + + // 图标逻辑 + let iconName = "rect.svg"; + switch (item.type) { + case 1: iconName = "images.svg"; break; + case 2: iconName = "images_add.svg"; break; + case 3: iconName = "text.svg"; break; + case 4: iconName = "text2.svg"; break; + case 5: iconName = "rect.svg"; break; + case 6: iconName = "round.svg"; break; + case 7: iconName = "line.svg"; break; + case 8: iconName = "qrcode.svg"; break; + case 9: iconName = "barcode.svg"; break; + case 10: iconName = "counter.svg"; break; + case 11: iconName = "text3.svg"; break; + } + + // 名称逻辑 + let nameText = item.name ? item.name : + (item.type === 3 ? "TEXT_" + displayIndex : + (item.type === 1 ? "IMG_" + displayIndex : "OBJ_" + displayIndex)); + + // 检查锁定状态(通过lockMovementX判断,因为锁定时会设置所有lock属性) + // 注意:索引0是背景图,objs数组是从下标0开始存第1个对象的,所以是 i+1 + let isLocked = false; + if (currentCanvas.getObjects()[i + 1]) { + let canvasObj = currentCanvas.getObjects()[i + 1]; + isLocked = canvasObj.get('lockMovementX') === true; + } + + // 锁定状态图标 + const lockIcon = isLocked ? + '' : + ''; + + // 组装 HTML + str = `
+
+ + ${nameText} + ${lockIcon} +
+ ${displayIndex} +
` + str; + i++; + } + $(".obj_list").html(str); + + // 重新绑定点击事件(防止 updateList 后点击失效) + $(".obj_list > div").on("click", function () { + let imgId = $(this).find("img").attr("id"); + if (imgId) { + let idx = parseInt(imgId.replace("obj", "")); + selectObj(idx); + } + }); +} + +// 标记是否通过对象列表选中(用于区分鼠标点击和列表点击) +window._selectingFromList = false; + +// 将 selectObj 附加到 window 对象,确保全局可访问 +window.selectObj = function selectObj(objIndices, isFromCanvas = false) { + let indicesArray = (Array.isArray(objIndices)) ? objIndices : ((typeof objIndices === 'number' && objIndices >= 0) ? [objIndices] : []); + + // 清除所有高亮 + $('.obj_list > div').css("background-color", "transparent"); + + // 遍历列表查找并高亮 + $('.obj_list > div').each(function () { + let $img = $(this).find("img"); + if ($img.length === 0) return; + + let idIndex = parseInt($img.attr("id").replace("obj", "")); + + if (indicesArray.includes(idIndex)) { + $(this).css("background-color", "#6F7A84"); + + // 自动滚动到可视区域 + if (isFromCanvas && indicesArray[0] === idIndex) { + this.scrollIntoView({ behavior: "smooth", block: "nearest" }); + } + } + }); + + // 如果是点击列表,触发画布选中 + if (!isFromCanvas && indicesArray.length === 1) { + let target = canvas.getObjects()[indicesArray[0] + 1]; + if (target) { + // 检查对象是否被锁定 + let isLocked = isObjectLocked(target); + + // 设置标志位,表示这是通过对象列表选中的(无论是否锁定) + window._selectingFromList = true; + + // 如果被锁定,临时设置 selectable 和 evented 为 true,以便能够选中 + if (isLocked) { + target.set({ + selectable: true, + evented: true + }); + } + + canvas.setActiveObject(target); + canvas.requestRenderAll(); + + // 如果对象被锁定,选中后立即恢复锁定状态(但保持选中状态) + if (isLocked) { + setTimeout(() => { + target.set({ + selectable: false, + evented: false + }); + canvas.requestRenderAll(); + }, 0); + } + + // 清除标志位(延迟清除,确保事件处理完成) + setTimeout(() => { + window._selectingFromList = false; + }, 10); + + handleObjectSelected(); + } + } +} + +// 将 recordState 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 +window.recordState = function recordState() { + // 撤销/重做过程中不记录历史,避免产生“中间状态”,导致需要点多次才能回到预期 + if (window._isApplyingHistory) { + return; + } + // 确定当前是正面还是背面,直接使用对应的数组 + const isFront = $('#front_side').hasClass('ui-button-active'); + const currentStep = isFront ? (window.step1 || step1) : (window.step2 || step2); + const currentRecordObjs = isFront ? (window.recordObjs1 || recordObjs1) : (window.recordObjs2 || recordObjs2); + const currentRecordJson = isFront ? (window.recordJson1 || recordJson1) : (window.recordJson2 || recordJson2); + const currentObjs = window.objs || objs; + const currentCanvas = window.canvas || canvas; + + ++currentStep.val; + for (let i = currentStep.val; i < currentRecordObjs.length; i++) { + currentRecordObjs.pop(); + currentRecordJson.pop(); + } + currentRecordObjs.push(JSON.stringify(currentObjs)) + // 保存所有锁定相关的属性,确保锁定状态可以正确恢复 + let j = currentCanvas.toJSON(TO_JSON_PROPERTIES); + j.objects[0].hoverCursor = "default"; + currentRecordJson.push(j); + + // 同步更新 window 变量和局部变量 + if (isFront) { + window.step1 = currentStep; + window.recordObjs1 = currentRecordObjs; + window.recordJson1 = currentRecordJson; + window.step = currentStep; + window.recordObjs = currentRecordObjs; + window.recordJson = currentRecordJson; + step1 = currentStep; + recordObjs1 = currentRecordObjs; + recordJson1 = currentRecordJson; + step = currentStep; + recordObjs = currentRecordObjs; + recordJson = currentRecordJson; + } else { + window.step2 = currentStep; + window.recordObjs2 = currentRecordObjs; + window.recordJson2 = currentRecordJson; + window.step = currentStep; + window.recordObjs = currentRecordObjs; + window.recordJson = currentRecordJson; + step2 = currentStep; + recordObjs2 = currentRecordObjs; + recordJson2 = currentRecordJson; + step = currentStep; + recordObjs = currentRecordObjs; + recordJson = currentRecordJson; + } +}; +// 向后兼容 +function recordState() { + return window.recordState(); +} + +// 将 getIndex 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 +window.getIndex = function getIndex(target, _canvas = null) { + let temp_objs; + // 使用 window.canvas 作为默认值,确保在 eval() 执行时也能访问 + const currentCanvas = _canvas || window.canvas || canvas; + if (!currentCanvas) { + return 0; + } + temp_objs = currentCanvas.getObjects(); + + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return i; + } + i++; + } + return 0; +}; +// 向后兼容 +function getIndex(target, _canvas = null) { + return window.getIndex(target, _canvas); +} + +function getType(target) { + // 如果是辅助线,返回 null + if (target.isGuideLine) { + return null; + } + let temp_objs = canvas.getObjects(); + let i = 0; + for (let res of temp_objs) { + if (target == res) { + // 安全检查:确保 objs[i] 存在且有 type 属性 + if (!objs[i] || typeof objs[i].type === 'undefined') { + return null; + } + return objs[i].type; + } + i++; + } + return null; +} + +function checkName(name) { + //检查字段名是否重复了 + for (let item of objs1) { + if (item.name == name) { + return false; + } + } + for (let item of objs2) { + if (item.name == name) { + return false; + } + } + return true; +} + +function resName(pre_name) { + //返回的是不重复的字段名 + let num = (objs1.length + objs2.length) - 1; + do { + num++; + } + while (!checkName(pre_name + num));//重复了就再加1 + return pre_name + num; +} + +function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 + var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 + var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 + return distance; +} + +function getDeg(pointer) { + // 计算点击位置与画布中心的坐标差 + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + // 计算旋转角度(弧度) + var angleRad = Math.atan2(dy, dx); + // 将角度转换为度数 + var angleDeg = angleRad * (180 / Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90 + return angleDeg +} + +function getCoordsMinX(acoords) { + x = acoords[0].x; + for (let item of acoords) { + if (item.x < x) { + x = item.x + } + } + return x; +} + +function getCoordsMaxX(acoords) { + x = acoords[0].x; + for (let item of acoords) { + if (item.x > x) { + x = item.x + } + } + return x; +} + +function getCoordsMinY(acoords) { + y = acoords[0].y; + for (let item of acoords) { + if (item.y < y) { + y = item.y + } + } + return y; +} + +function getCoordsMaxY(acoords) { + y = acoords[0].y; + for (let item of acoords) { + if (item.y > y) { + y = item.y + } + } + return y; +} + +function addBackground() { + // 类型改变 + fabric.Image.fromURL('./public/images/bg_front_2.png', function (image) { + image.left = ($('#canvas1').width() - image.width * image.get('scaleX')) / 2 + image.top = ($('#canvas1').height() - image.height * image.get('scaleY')) / 2 + + image.selectable = false + image.hoverable = false + image.hoverCursor = 'default' + image.setSrc(image.toDataURL(), function (image) { + background_image1 = image + background_image = background_image1 + window.background_image1 = background_image1; + window.background_image = background_image; + canvas1.add(image) + canvas1.renderAll() + + recordObjs1.push(JSON.stringify(objs1)) + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; + recordJson1.push(canvas1.toJSON(props)) + }) + // 类型改变 + fabric.Image.fromURL('./public/images/bg_back_2.png', function (image) { + image.left = ($('#canvas2').width() - image.width) / 2 + image.top = ($('#canvas2').height() - image.height) / 2 + + image.selectable = false + image.hoverable = false + image.hoverCursor = 'default' + + image.setSrc(image.toDataURL(), function (image) { + background_image2 = image + window.background_image2 = background_image2; + canvas2.add(image) + canvas2.renderAll() + + recordObjs2.push(JSON.stringify(objs2)) + recordJson2.push(canvas2.toJSON(TO_JSON_PROPERTIES)) + + let file = GetFile().get('file') + if (file && file != 'empty') { + window.openFile(file); + } + }) + }) + is_bgi_add = true + }) +} + +function addPic(pointer) { + pre_add_image.left = pointer.x + pre_add_image.top = pointer.y + + canvas.add(pre_add_image) + + objs.push({ type: 1, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +function addOutPic(pointer) { + fabric.Image.fromURL('./public/images/outpic_extra.png', function (image) { + let width = canvas.getWidth() + let height = canvas.getHeight() + image.left = pointer.x + image.top = pointer.y + image.opacity = 1 + image.setSrc(image.toDataURL(), function (image) { + canvas.add(image) + objs.push({ type: 2, name: resName('OUT_PIC_'), black: false }) + canvas.renderAll() + updateList() + recordState() + }) + }) +} + +function addText(pointer) { + // 设置默认字号为30px(像素值),这样显示的字号约为 30 * 72 / 300 = 7.2pt + let text = new fabric.IText('TEXT', { + left: pointer.x, + top: pointer.y, + fill: '#000000', + textBackgroundColor: '', + fontStyle: 'normal', + fontWeight: 'normal', + fontSize: 30, + underline: false, + linethrough: false, + fontFamily: 'Arial' + }) + text.setControlVisible('ml', false) + text.setControlVisible('mt', false) + text.setControlVisible('mr', false) + text.setControlVisible('mb', false) + canvas.add(text) + objs.push({ type: 3, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +function addCircleText(pointer) { + let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2) + let angleDeg = getDeg(pointer) + let text = new fabric.CurvedText('Circle TEXT', { + left: pointer.x, + top: pointer.y, + fill: "#000000", + textBackgroundColor: "", + fontStyle: "normal", + fontWeight: "normal", + diameter: distance * 2, + fontSize: 30, + underline: false, + linethrough: false, + lockScalingX: true, + lockScalingY: true, + fontFamily: 'Arial', + originX: 'center', + originY: 'center', + angle: angleDeg, + flipped: pointer.y > canvas.height / 2 ? true : false + }); + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + canvas.add(text); + objs.push({ type: 11, black: false, circleCenter: 0 }); + canvas.renderAll(); + updateList(); + recordState(); +} + +function addOutText(pointer) { + let text = new fabric.Text('MERGE TEXT', { + left: pointer.x, + top: pointer.y, + fill: '#000000', + fontStyle: 'normal', + fontWeight: 'normal', + textBackgroundColor: '', + underline: false, + linethrough: false, + fontFamily: 'Arial', + textAlign: 'center' + }) + text.setControlVisible('ml', false) + text.setControlVisible('mt', false) + text.setControlVisible('mr', false) + text.setControlVisible('mb', false) + + canvas.add(text) + objs.push({ type: 4, name: resName('OUT_TEXT_'), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' }) + canvas.renderAll() + updateList() + recordState() +} + +function addRect(pointer) { + let rect = new fabric.Rect({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: "#ff0000", + strokeWidth: 3, + width: 100, + height: 100 + }) + canvas.add(rect) + objs.push({ type: 5, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +function addCircle(pointer) { + let circle = new fabric.Circle({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: '#ff0000', + strokeWidth: 3, + radius: 100, + strokeUniform: true + }) + canvas.add(circle) + objs.push({ type: 6, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +function addLine(pointer) { + let line = new fabric.Line([50, 100, 200, 100], { + left: pointer.x, + top: pointer.y, + stroke: '#ff0000', + strokeWidth: 3, + strokeDashArray: [0, 0], + }) + + line.setControlVisible('tl', false) + line.setControlVisible('tr', false) + line.setControlVisible('br', false) + line.setControlVisible('bl', false) + line.setControlVisible('mt', false) + line.setControlVisible('mb', false) + canvas.add(line) + objs.push({ type: 7, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +function addQrCode(pointer) { + let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 }) + fabric.Image.fromURL(qr, function (image) { + image.left = pointer.x + image.top = pointer.y + canvas.add(image) + objs.push({ type: 8, name: resName('QR_CODE_'), color: '#000000', val: 'http://www.cardsoon.com', black: false }) + canvas.renderAll() + updateList() + recordState() + }) +} + +function addBarCode(pointer) { + JsBarcode('#barcode', '123456789', { margin: 0, lineColor: '#000000', fontSize: 18, font: 'Arial' }) + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + fabric.Image.fromURL(bar, function (image) { + image.left = pointer.x + image.top = pointer.y + + canvas.add(image) + objs.push({ type: 9, name: resName('BAR_CODE_'), val: '123456789', color: '#000000', fontSize: 18, font: 'Arial', show: false, black: false }) + canvas.renderAll() + updateList() + recordState() + }) +} + +function addCounter(pointer) { + let text = new fabric.Text('0', { + left: pointer.x, + top: pointer.y, + fill: '#000000' + }) + canvas.add(text) + objs.push({ type: 10, name: resName('COUNTER_'), start: 0, add: 1, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +var front_list = [] +ipcRenderer.on('font-list', (event, data) => { + const newFontList = [] + data.map((item) => { + if (item.indexOf('"') === 0) { + newFontList.push(item.replace(/^"|"$/g, '')) + } else { + newFontList.push(item) + } + }) + front_list = newFontList + for (let item of front_list) { + $('#text_font_family').append("') + $("#circle_text_font_family").append(""); + $('#out_text_font_family').append("') + $('#barcode_font_family').append("') + $('#count_font_family').append("') + } +}) +ipcRenderer.on('sys-lan', (event, data) => { + let lang = localStorage.getItem('lang') + if (lang) { + s_lan = lang + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge(lang) + $('#language_select').val(lang) + return + } + switch (data) { + case 'zh-CN': + s_lan = 'zh' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('zh') + $('#language_select').val('zh') + localStorage.setItem('lang', 'zh') + break + case 'zh': + s_lan = 'zh' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('zh') + $('#language_select').val('zh') + localStorage.setItem('lang', 'zh') + break + case 'zh-TW': + s_lan = 'ozh' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('ozh') + $('#language_select').val('ozh') + localStorage.setItem('lang', 'ozh') + break + case 'en': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-AU': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-CA': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-GB': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-NZ': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-US': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-ZA': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + } +}) +ipcRenderer.on('close', (event, message) => { + layer.confirm( + '' + language_str('whetherSave') + '', + { + //是否保存当前文件? + btn: [language_str('save'), language_str('noSave'), language_str('cancel')], //['保存', '不保存', '取消'] + btn3: function (index, layero) { } + }, + function (index, layero) { + if (typeof window.output === 'function') { + window.output(() => ipcRenderer.send('run-close')); + } + }, + function (index) { + ipcRenderer.send('run-close') + } + ) +}) + +addBackground() //添加背景 白色卡片 + +initAligningGuidelines(canvas1) +initAligningGuidelines(canvas2) + +// 初始化 Canvas 事件监听 +initCanvasEvents(); + +// 初始化所有 Slider +var circle_text_size_updating = false; // 标志位,防止循环更新 +var circle_text_size = slider.render({ + elem: '#circle_text_size', + min: 10, + max: 120, + value: 50, + change: function (value) { + if (circle_text_size_updating) return; + if (canvas && canvas.getActiveObject && canvas.getActiveObject()) { + canvas.getActiveObject().set('fontSize', value); + canvas.renderAll(); + if (typeof updateControls === 'function') { + updateControls(); + } + if (typeof recordState === 'function') { + recordState(); + } + } + } +}); + +var circle_text_diameter = slider.render({ + elem: '#circle_text_diameter', + min: 50, + max: 1100, + value: 500, + change: function (value) { + if (canvas && canvas.getActiveObject && canvas.getActiveObject()) { + canvas.getActiveObject().set('diameter', value); + canvas.renderAll(); + if (typeof updateControls === 'function') { + updateControls(); + } + if (typeof recordState === 'function') { + recordState(); + } + } + } +}); + +var out_pic_op = slider.render({ + elem: '#out_pic_op', + min: 0, + max: 100, + value: 0, + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set('opacity', 1 - value / 100); + canvas.renderAll(); + updateControls(); + recordState(); + } + } +}); + +var pic_op = slider.render({ + elem: '#pic_op', + min: 0, + max: 100, + value: 0, + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set('opacity', 1 - value / 100); + canvas.renderAll(); + updateControls(); + recordState(); + } + } +}); + +var rect_op = slider.render({ + elem: '#rect_op', + min: 0, + max: 100, + value: 0, + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set('opacity', 1 - value / 100); + canvas.renderAll(); + updateControls(); + recordState(); + } + } +}); + +var circle_op = slider.render({ + elem: '#circle_op', + min: 0, + max: 100, + value: 0, + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set('opacity', 1 - value / 100); + canvas.renderAll(); + updateControls(); + recordState(); + } + } +}); + +//canvas.setZoom(0.5) +//recordObjs1.push(JSON.stringify(objs1)); +//recordJson1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); +//recordObjs2.push(JSON.stringify(objs2)); +//recordJson2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); +canvas.renderAll() //重新渲染画布 +var zoomSlider = slider.render({ + elem: '#zoomSlide', //绑定元素 + min: 25, + max: 500, + value: 100, + change(val) { + canvas.zoomToPoint(new fabric.Point(canvas.width / 2, canvas.height / 2), val / 100) + zoom = val / 100 + //$(".canvas-container").css("transform","scale(" + val / 100 + ")") + } +}) +window.rotate = function (id) { + let angle = $(`#${id}`).attr('angle') - 0 + angle += 90 + $(`#${id}`).css('transform', `rotate(${angle}deg)`) + $(`#${id}`).attr('angle', angle) +} +// 注意:#display 事件绑定已在 ui.js 中定义(第2633行),此处已删除重复定义 +// 导出文件 +// 将 savePdf 附加到 window 对象,确保在 output.js 中也能访问 +window.savePdf = async function savePdf(buffer) { + let fs = require('fs'); + // 弹出保存对话框 + const { canceled, filePath } = await dialog.showSaveDialog({ + title: '保存文件', + defaultPath: path.join(exePath, 'output.pdf'), // 默认路径 + filters: [{ name: 'PDF 文件', extensions: ['pdf'] }] // 文件过滤器 + }); + + if (canceled) { + return; + } + + // 将Buffer写入PDF文件到用户选择的路径 + fs.writeFile(filePath, buffer, (err) => { + if (err) { + } else { + layer.msg(language_str("saveSucc") + filePath);//'保存成功至' + } + }); +}; +// 向后兼容 +async function savePdf(buffer) { + return window.savePdf(buffer); +} + +// display_func 函数已移至 output.js,避免重复定义 +// function display_func(img1, img2, img3) { ... } + +// output 函数已移至 output.js,避免重复定义 +// function output(callback = null, _save = save) { ... } + +var _keyctrl = false, + _keyc = false, + _textEdit = false, + _inputEdit = false +$('input').focus(function () { + _inputEdit = true +}) +$('input').blur(function () { + _inputEdit = false +}) +$('textarea').focus(function () { + _inputEdit = true +}) +$('textarea').blur(function () { + _inputEdit = false +}) +$('body').keyup(function (event) { + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释放 + _keyctrl = false + } + if (event.keyCode == 67) { + _keyc = false + } +}) +$('body').keydown(function (event) { + // 统一的快捷键处理(兼容 Windows/Linux: Ctrl, Mac: Cmd/Meta) + const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 + + // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的) + if (!_inputEdit && !_textEdit) { + // CTRL/CMD+N - 新建 + if (isModifierKey && event.keyCode == 78) { + event.preventDefault(); + $('#new').click(); + return; + } + // CTRL/CMD+O - 打开 + if (isModifierKey && event.keyCode == 79) { + event.preventDefault(); + $('#open').click(); + return; + } + // CTRL/CMD+S - 保存 + if (isModifierKey && !event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + $('#save').click(); + return; + } + // CTRL/CMD+SHIFT+S - 保存副本 + if (isModifierKey && event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + if (typeof window.output === 'function') { + window.output(null, saveAs); + } + return; + } + // CTRL/CMD+Z - 还原 + if (isModifierKey && !event.shiftKey && event.keyCode == 90) { + event.preventDefault(); + $('#previous').click(); + return; + } + // CTRL/CMD+Y - 重做 + if (isModifierKey && !event.shiftKey && event.keyCode == 89) { + event.preventDefault(); + $('#next').click(); + return; + } + // CTRL/CMD+C - 复制 + if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { + event.preventDefault(); + window.copySelection(); + return; + } + // CTRL/CMD+V - 粘贴 + if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { + event.preventDefault(); + window.pasteFromClipboard(); + return; + } + // CTRL/CMD+SHIFT+DEL - 清空 + if (isModifierKey && event.shiftKey && event.keyCode == 46) { + event.preventDefault(); + $('#clean').click(); + return; + } + // CTRL/CMD+DEL - 删除 + if (isModifierKey && !event.shiftKey && event.keyCode == 46) { + event.preventDefault(); + $('#delete').click(); + return; + } + } + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd) + _keyctrl = true + if (_keyc && _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 67) { + _keyc = true + if (_keyctrl & _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 37) { + //left + let v = -1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5 + } + if (canvas.getActiveObject()) { + let x = canvas.getActiveObject().get('left') + x += v + canvas.getActiveObject().set('left', x) + canvas.renderAll() + recordState() + updateControls() + } + } + if (event.keyCode == 38) { + //up + let v = -1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5 + } + if (canvas.getActiveObject()) { + let y = canvas.getActiveObject().get('top') + y += v + canvas.getActiveObject().set('top', y) + canvas.renderAll() + recordState() + updateControls() + } + } + if (event.keyCode == 39) { + //right + let v = 1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5 + } + if (canvas.getActiveObject()) { + let x = canvas.getActiveObject().get('left') + x += v + canvas.getActiveObject().set('left', x) + canvas.renderAll() + recordState() + updateControls() + } + } + if (event.keyCode == 40) { + //down + let v = +1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5 + } + if (canvas.getActiveObject()) { + let y = canvas.getActiveObject().get('top') + y += v + canvas.getActiveObject().set('top', y) + canvas.renderAll() + recordState() + updateControls() + } + } + // 普通删除键(DEL键,没有按CTRL/CMD) + if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { + let actObjs = canvas.getActiveObjects() + let allObjs = canvas.getObjects() + let flag = 0 + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item, canvas) //0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1) + canvas.remove(item) + flag = 1 + } + } + updateList() + canvas.discardActiveObject() + if (flag == 1) { + recordState() + canvas.renderAll() + handleObjectSelected() + } + } +}) + +// 复制选中对象 +window.copySelection = function copySelection() { + const activeObjects = canvas.getActiveObjects(); + + if (!activeObjects || activeObjects.length === 0) { + layer.msg("未选中任何对象") + return; + } + + // 【核心修复】遍历选中的对象,同时保存它的 Fabric 画面数据 和 我们的 objs 业务数据 + clipboardData.data = activeObjects.map(obj => { + let idx = getIndex(obj, canvas); // 获取对象在画布中的索引 + + // 获取业务属性 (objs数组中对应的那一项) + // 注意:idx=0 是背景图,objs数组是从下标0开始存第1个对象的,所以是 idx-1 + let appData = (idx > 0 && objs[idx - 1]) ? objs[idx - 1] : { type: 0 }; + + return { + // 1. 保存画面样式 (坐标、颜色、字号等,包含图片的 src) + fabricJson: obj.toObject(['id', 'name', 'selectable', 'evented', 'data', 'src']), + // 2. 保存业务属性 (深拷贝,防止引用冲突) + appData: JSON.parse(JSON.stringify(appData)) + }; + }); + + clipboardData.offset = 10; // 重置偏移量 + layer.msg("已复制"); +} + +// 粘贴对象 +window.pasteFromClipboard = function pasteFromClipboard() { + // 检查是否有数据 + if (!clipboardData.data || clipboardData.data.length === 0) { + layer.msg("剪贴板为空") + return; + } + + // 防止重复调用:如果正在粘贴,直接返回 + if (window._isPasting) { + return; + } + window._isPasting = true; + + // 1. 提取 Fabric JSON 准备恢复画面 + const jsonToEnliven = clipboardData.data.map(item => item.fabricJson); + + // 2. 恢复对象 (异步过程) + fabric.util.enlivenObjects(jsonToEnliven, function (objects) { + canvas.discardActiveObject(); + + objects.forEach((clone, index) => { + // --- A. 处理画面位置 --- + clone.set({ + left: clone.left + clipboardData.offset, + top: clone.top + clipboardData.offset, + evented: true + }); + + // 如果是组内对象,需要特殊处理坐标 + if (clone.group) { + clone.set({ + left: clone.group.left + clipboardData.offset, + top: clone.group.top + clipboardData.offset + }); + } + + // --- B. 处理业务数据 --- + // 取出复制时保存的 appData + let savedAppData = clipboardData.data[index].appData; + let newAppData = JSON.parse(JSON.stringify(savedAppData)); // 再次深拷贝 + + // 修改名称,防止 ID 冲突 (例如 TEXT_1 -> TEXT_1_copy) + if (newAppData.name) { + newAppData.name = newAppData.name + "_copy"; + } + + // --- C. 添加到画布和数据数组 --- + canvas.add(clone); + objs.push(newAppData); // 将新业务数据推入当前页面的 objs 数组 + }); + + // 3. 选中新粘贴出来的对象 + let sel = new fabric.ActiveSelection(objects, { canvas: canvas }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + + // 4. 刷新列表和历史记录 + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState(); + + // 5. 高亮左侧列表最后一项 (即新粘贴的项) + if (typeof window.selectObj === 'function') { + if (objs.length > 0) { + window.selectObj(objs.length - 1); + } + } + + // 6. 增加下次粘贴的偏移量 (让用户知道粘贴了新东西) + clipboardData.offset += 10; + + // 清除粘贴标志 + window._isPasting = false; + }); +} + +// 将 onStateRestored 附加到 window 对象,确保全局可访问 +// 此函数在撤回/重做后调用,用于恢复画布状态 +window.onStateRestored = function onStateRestored() { + // 1. 重新获取背景图引用 (因为旧对象被销毁了) + // 注意:canvas 是全局变量,指向当前激活的画板 + if (canvas && canvas.getObjects().length > 0) { + background_image = canvas.getObjects()[0]; + background_image.selectable = false; // 确保背景不可选 + window.background_image = background_image; + background_image.evented = false; + } + + // 2. 渲染画布 + canvas.requestRenderAll(); + + // 3. 取消选中状态 + if (typeof window.selectObj === 'function') { + window.selectObj(-1); + } + + // 4. 隐藏所有属性面板 + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + + // 5. 刷新左侧列表 + if (typeof window.updateList === 'function') { + window.updateList(); + } +} diff --git a/lib/design2/output.js b/lib/design2/output.js index e957e3f..c9462aa 100644 --- a/lib/design2/output.js +++ b/lib/design2/output.js @@ -1246,7 +1246,111 @@ window.openFile = function open(file) { } }) + // 先清空历史记录和重置 step + objs2 = j.bo || [] + objs1 = j.fo || [] + step1.val = 0 + step2.val = 0 + recordJson1 = [] + recordJson2 = [] + recordObjs1 = [] + recordObjs2 = [] + + // 同步更新 window 变量 + window.objs1 = objs1 + window.objs2 = objs2 + window.step1 = step1 + window.step2 = step2 + window.recordJson1 = recordJson1 + window.recordJson2 = recordJson2 + window.recordObjs1 = recordObjs1 + window.recordObjs2 = recordObjs2 + + if ($('#front_side').hasClass('ui-button-active')) { + objs = objs1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + window.objs = objs1 + window.recordObjs = recordObjs1 + window.recordJson = recordJson1 + window.step = step1 + } else if ($('#back_side').hasClass('ui-button-active')) { + objs = objs2 + recordObjs = recordObjs2 + recordJson = recordJson2 + background_image = background_image2 + step = step2 + window.objs = objs2 + window.recordObjs = recordObjs2 + window.recordJson = recordJson2 + window.step = step2 + } + // 加载 JSON,并在回调中重新生成二维码和条形码 + // 使用 Promise 确保两个画布都加载完成后再保存初始状态 + let canvas1Loaded = false + let canvas2Loaded = false + + function saveInitialState() { + // 两个画布都加载完成后才保存初始状态 + if (!canvas1Loaded || !canvas2Loaded) return + + // 确保使用正确的 objs1 和 objs2(从文件加载的 fo 和 bo) + // 注意:必须使用 fo 和 bo,因为它们是直接从文件 JSON 中读取的 + // 同时确保 objs1 和 objs2 也被正确设置 + objs1 = fo || [] + objs2 = bo || [] + window.objs1 = objs1 + window.objs2 = objs2 + + // 更新背景图引用 + background_image1 = canvas1.getObjects()[0] + background_image2 = canvas2.getObjects()[0] + window.background_image1 = background_image1 + window.background_image2 = background_image2 + + if ($('#front_side').hasClass('ui-button-active')) { + background_image = background_image1 + window.background_image = background_image1 + objs = objs1 + window.objs = objs1 + } else if ($('#back_side').hasClass('ui-button-active')) { + background_image = background_image2 || background_image1 + window.background_image = background_image + objs = objs2 + window.objs = objs2 + } + + // 保存初始状态(使用从文件加载的 objs 数组) + recordObjs1.push(JSON.stringify(objs1)) + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; + let j1 = canvas1.toJSON(props) + j1.objects[0].hoverCursor = 'default' + recordJson1.push(j1) + + recordObjs2.push(JSON.stringify(objs2)) + let j2 = canvas2.toJSON(props) + j2.objects[0].hoverCursor = 'default' + recordJson2.push(j2) + + // 同步更新 window 变量和局部变量 + window.recordObjs1 = recordObjs1 + window.recordJson1 = recordJson1 + window.recordObjs2 = recordObjs2 + window.recordJson2 = recordJson2 + // 确保局部变量也同步 + if ($('#front_side').hasClass('ui-button-active')) { + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + } else if ($('#back_side').hasClass('ui-button-active')) { + recordObjs = recordObjs2 + recordJson = recordJson2 + step = step2 + } + } + canvas1.loadFromJSON(j.f, function () { // 加载后解锁所有对象(背景除外) if (typeof window.unlockAllObjects === 'function') { @@ -1260,6 +1364,8 @@ window.openFile = function open(file) { window.updateList(); } canvas1.renderAll() + canvas1Loaded = true + saveInitialState() }) canvas2.loadFromJSON(j.b, function () { // 加载后解锁所有对象(背景除外) @@ -1274,59 +1380,9 @@ window.openFile = function open(file) { window.updateList(); } canvas2.renderAll() + canvas2Loaded = true + saveInitialState() }) - background_image1 = canvas1.getObjects()[0] - background_image2 = canvas2.getObjects()[0] - - objs2 = j.bo - objs1 = j.fo - step1.val = 0 - step2.val = 0 - recordJson1 = [] - recordJson2 = [] - recordObjs1 = [] - recordObjs2 = [] - if ($('#front_side').hasClass('ui-button-active')) { - objs = objs1 - recordObjs = recordObjs1 - recordJson = recordJson1 - step = step1 - background_image = background_image1 - } else if ($('#back_side').hasClass('ui-button-active')) { - objs = objs2 - recordObjs = recordObjs2 - recordJson = recordJson2 - background_image = background_image2 - step = step2 - } - canvas1.renderAll() - canvas2.renderAll() - // 调用 updateList,现在它已附加到 window 对象 - if (typeof window.updateList === 'function') { - window.updateList(); /* - 修复打开新文件后无法撤销的bug - 2022-07-16 - */ - } else { - // 延迟检查,确保 core.js 已加载 - setTimeout(() => { - if (typeof window.updateList === 'function') { - window.updateList(); - } - }, 100); - } - setTimeout(() => { - recordObjs1.push(JSON.stringify(objs1)) - const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented']; - let j1 = canvas1.toJSON(props) - j1.objects[0].hoverCursor = 'default' - recordJson1.push(j1) - - recordObjs2.push(JSON.stringify(objs2)) - let j2 = canvas2.toJSON(props) - j2.objects[0].hoverCursor = 'default' - recordJson2.push(j2) - }, 0) } function clearAll() { diff --git a/lib/design2/ui.js b/lib/design2/ui.js index f7b97ce..2cdaf80 100644 --- a/lib/design2/ui.js +++ b/lib/design2/ui.js @@ -28,69 +28,10 @@ $('body').keydown(function (event) { const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的) + // 注意:大部分快捷键已在 core.js 中统一处理,此处只保留 ui.js 特有的快捷键 + // 已移除的重复快捷键:CTRL/CMD+N/O/S/Z/Y/C/V/SHIFT+DEL/DEL(已在 core.js 中处理) if (!_inputEdit && !_textEdit) { - // CTRL/CMD+N - 新建 - if (isModifierKey && event.keyCode == 78) { - event.preventDefault(); - $('#new').click(); - return; - } - // CTRL/CMD+O - 打开 - if (isModifierKey && event.keyCode == 79) { - event.preventDefault(); - $('#open').click(); - return; - } - // CTRL/CMD+S - 保存 - if (isModifierKey && !event.shiftKey && event.keyCode == 83) { - event.preventDefault(); - $('#save').click(); - return; - } - // CTRL/CMD+SHIFT+S - 保存副本 - if (isModifierKey && event.shiftKey && event.keyCode == 83) { - event.preventDefault(); - if (typeof window.output === 'function') { - window.output(null, saveAs); - } - return; - } - // CTRL/CMD+Z - 还原 - if (isModifierKey && !event.shiftKey && event.keyCode == 90) { - event.preventDefault(); - $('#previous').click(); - return; - } - // CTRL/CMD+Y - 重做 - if (isModifierKey && !event.shiftKey && event.keyCode == 89) { - event.preventDefault(); - $('#next').click(); - return; - } - // CTRL/CMD+C - 复制 - if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { - event.preventDefault(); - window.copySelection(); - return; - } - // CTRL/CMD+V - 粘贴 - if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { - event.preventDefault(); - window.pasteFromClipboard(); - return; - } - // CTRL/CMD+SHIFT+DEL (Windows) 或 CMD+SHIFT+Backspace (Mac) - 清空 - if (isModifierKey && event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { - event.preventDefault(); - $('#clean').click(); - return; - } - // CTRL/CMD+DEL (Windows) 或 CMD+Backspace (Mac) - 删除 - if (isModifierKey && !event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { - event.preventDefault(); - $('#delete').click(); - return; - } + // ui.js 特有的快捷键可以在这里添加 } // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd) @@ -439,6 +380,10 @@ $('#front_side').click(function () { recordObjs = recordObjs1 recordJson = recordJson1 step = step1 + // 同步到 window 对象,确保 recordState 使用正确的数组 + window.recordObjs = recordObjs1 + window.recordJson = recordJson1 + window.step = step1 if (typeof window.updateList === 'function') { window.updateList(); } @@ -476,6 +421,10 @@ $('#back_side').click(function () { recordObjs = recordObjs2 recordJson = recordJson2 step = step2 + // 同步到 window 对象,确保 recordState 使用正确的数组 + window.recordObjs = recordObjs2 + window.recordJson = recordJson2 + window.step = step2 if (typeof window.updateList === 'function') { window.updateList(); } @@ -894,147 +843,155 @@ $('#set_bg').click(function () { } }) $('#previous').click(function () { + // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 + if (window._isApplyingHistory) { + return; + } //撤销一次操作 if (step.val == 0) { return } step.val-- + if ($('#front_side').hasClass('ui-button-active')) { // 使用 recordJson1 和 recordObjs1 + if (recordObjs1 && recordObjs1[step.val]) { + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + // 同步更新 window 变量 + window.objs1 = objs1; + window.objs = objs; + } + if (recordJson1 && recordJson1[step.val]) { + window._isApplyingHistory = true; canvas.loadFromJSON(recordJson1[step.val], function() { // 加载后解锁所有对象(背景除外) if (typeof window.unlockAllObjects === 'function') { window.unlockAllObjects(canvas); } - // 加载后更新列表和图标显示 - if (typeof window.updateList === 'function') { - window.updateList(); + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs1); } - canvas.renderAll(); - }) - } - if (recordObjs1 && recordObjs1[step.val]) { - objs1 = JSON.parse(recordObjs1[step.val]) - objs = objs1 + // 调用状态恢复函数 + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); } } else if ($('#back_side').hasClass('ui-button-active')) { // 使用 recordJson2 和 recordObjs2 + if (recordObjs2 && recordObjs2[step.val]) { + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + // 同步更新 window 变量 + window.objs2 = objs2; + window.objs = objs; + } + if (recordJson2 && recordJson2[step.val]) { + window._isApplyingHistory = true; canvas.loadFromJSON(recordJson2[step.val], function() { // 加载后解锁所有对象(背景除外) if (typeof window.unlockAllObjects === 'function') { window.unlockAllObjects(canvas); } - // 加载后更新列表和图标显示 - if (typeof window.updateList === 'function') { - window.updateList(); + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs2); } - canvas.renderAll(); - }) - } - if (recordObjs2 && recordObjs2[step.val]) { - objs2 = JSON.parse(recordObjs2[step.val]) - objs = objs2 + // 调用状态恢复函数 + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); } } - - background_image = canvas.getObjects()[0] - canvas.renderAll() - //recordState(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - - selectObj(-1) - - $('#base_control').hide() - $('#line_control').hide() - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() }) $('#next').click(function () { + // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 + if (window._isApplyingHistory) { + return; + } //撤销一次撤销操作 // 根据当前是正面还是背面检查对应的 recordObjs 长度 + let maxLen; if ($('#front_side').hasClass('ui-button-active')) { - if (step.val >= recordObjs1.length - 1) { + maxLen = recordObjs1 ? recordObjs1.length : 0; + if (step.val >= maxLen - 1) { return } } else if ($('#back_side').hasClass('ui-button-active')) { - if (step.val >= recordObjs2.length - 1) { + maxLen = recordObjs2 ? recordObjs2.length : 0; + if (step.val >= maxLen - 1) { return } + } else { + return; } ++step.val + if ($('#front_side').hasClass('ui-button-active')) { // 使用 recordJson1 和 recordObjs1 + if (recordObjs1 && recordObjs1[step.val]) { + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + // 同步更新 window 变量 + window.objs1 = objs1; + window.objs = objs; + } + if (recordJson1 && recordJson1[step.val]) { + window._isApplyingHistory = true; canvas.loadFromJSON(recordJson1[step.val], function() { // 加载后解锁所有对象(背景除外) if (typeof window.unlockAllObjects === 'function') { window.unlockAllObjects(canvas); } - // 加载后更新列表和图标显示 - if (typeof window.updateList === 'function') { - window.updateList(); + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs1); } - canvas.renderAll(); - }) - } - if (recordObjs1 && recordObjs1[step.val]) { - objs1 = JSON.parse(recordObjs1[step.val]) - objs = objs1 + // 调用状态恢复函数 + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); } } else if ($('#back_side').hasClass('ui-button-active')) { // 使用 recordJson2 和 recordObjs2 + if (recordObjs2 && recordObjs2[step.val]) { + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + // 同步更新 window 变量 + window.objs2 = objs2; + window.objs = objs; + } + if (recordJson2 && recordJson2[step.val]) { + window._isApplyingHistory = true; canvas.loadFromJSON(recordJson2[step.val], function() { // 加载后解锁所有对象(背景除外) if (typeof window.unlockAllObjects === 'function') { window.unlockAllObjects(canvas); } - // 加载后更新列表和图标显示 - if (typeof window.updateList === 'function') { - window.updateList(); + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs2); } - canvas.renderAll(); - }) + // 调用状态恢复函数 + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); } - if (recordObjs2 && recordObjs2[step.val]) { - objs2 = JSON.parse(recordObjs2[step.val]) - objs = objs2 - } - } - background_image = canvas.getObjects()[0] - canvas.renderAll() - selectObj(-1) - - $('#base_control').hide() - $('#line_control').hide() - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - if (typeof window.updateList === 'function') { - window.updateList(); } }) $('#clean').click(function () { @@ -1316,8 +1273,9 @@ $('#circle_stroke_transparent').click(function () { // cleanupSrcFields 函数已在 output.js 中定义 function saveAs(op1, callback) { - let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; + let j = canvas1.toJSON(props) + let b = canvas2.toJSON(props) // 清理不需要的 src 字段(使用 window.cleanupSrcFields,在 output.js 中定义) if (typeof window.cleanupSrcFields === 'function') { @@ -1464,7 +1422,7 @@ function addBackground() { //pre_objs1.push(JSON.stringify(objs1)); recordObjs1.push(JSON.stringify(objs1)) - const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented']; + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; recordJson1.push(canvas1.toJSON(props)) }) // 类型改变 @@ -1483,7 +1441,7 @@ function addBackground() { //pre_objs2.push(JSON.stringify(objs2)); recordObjs2.push(JSON.stringify(objs2)) - const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented']; + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; recordJson2.push(canvas2.toJSON(props)) let file = GetFile().get('file') @@ -1635,6 +1593,7 @@ function selectOutPic(target, index) { } function addText(pointer) { + // 设置默认字号为30px(像素值),这样显示的字号约为 30 * 72 / 300 = 7.2pt let text = new fabric.IText('TEXT', { //源:文本 left: pointer.x, @@ -1643,6 +1602,7 @@ function addText(pointer) { textBackgroundColor: '', fontStyle: 'normal', fontWeight: 'normal', + fontSize: 30, underline: false, linethrough: false, fontFamily: 'Arial' @@ -2405,7 +2365,8 @@ $("#circle_text_back_color").bind('input propertychange', function () { }) $("#circle_text_text").bind('input propertychange', function () { let val = $("#circle_text_text").val().trim() - canvas.getActiveObject().set("text", ` ${val} `); + // 有内容时不加首行缩进(前面的空格),只保留后面的空格 + canvas.getActiveObject().set("text", val ? `${val} ` : ` ${val} `); canvas.renderAll(); if (typeof window.updateList === 'function') { window.updateList(); @@ -2448,7 +2409,8 @@ $("#out_text_size").bind('input propertychange', function () { }) $('#out_text_text').bind('input propertychange', function () { let val = $("#out_text_text").val().trim() - canvas.getActiveObject().set("text", ` ${val} `); + // 有内容时不加首行缩进(前面的空格),只保留后面的空格 + canvas.getActiveObject().set("text", val ? `${val} ` : ` ${val} `); canvas.renderAll() if (typeof window.updateList === 'function') { window.updateList(); @@ -2565,7 +2527,8 @@ $('#line_dist').bind('input propertychange', function () { }) $('#text_text').bind('input propertychange', function () { let val = $("#text_text").val().trim() - canvas.getActiveObject().set("text", ` ${val} `); + // 有内容时不加首行缩进(前面的空格),只保留后面的空格 + canvas.getActiveObject().set("text", val ? `${val} ` : ` ${val} `); canvas.renderAll() if (typeof window.updateList === 'function') { window.updateList(); diff --git a/package arm64.json b/package arm64.json index 987591e..6b1838a 100644 --- a/package arm64.json +++ b/package arm64.json @@ -8,7 +8,7 @@ "package:x64": "electron-forge package --arch=x64", "package:arm64": "electron-forge package --arch=arm64", "merge": "node ./merge.js", - "package:universal": "npm run package:x64 && npm run package:arm64 && npm run merge" + "package:universal": "yarn package:x64 && yarn package:arm64 && yarn merge" }, "electronPackagerConfig": { "packageManager": "npm", diff --git a/package.json b/package.json index 78c8a19..d7677d5 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "build:linux": "electron-builder --linux --x64", "build:linux:arm64": "electron-builder --linux --arm64", "build:all": "electron-builder -mwl", - "release:win": "npm run encrypt:win && npm run cleanup && npm run build:win", - "release:mac": "npm run encrypt:linux && npm run cleanup && npm run build:mac", - "release:linux": "npm run encrypt:linux && npm run cleanup && npm run build:linux", - "release:linux:arm64": "npm run build:linux:arm64" + "release:win": "yarn encrypt:win && yarn cleanup && yarn build:win", + "release:mac": "yarn encrypt:linux && yarn cleanup && yarn build:mac", + "release:linux": "yarn encrypt:linux && yarn cleanup && yarn build:linux", + "release:linux:arm64": "yarn build:linux:arm64" }, "build": { "productName": "SoonDesign", @@ -103,7 +103,7 @@ } }, "electronPackagerConfig": { - "packageManager": "npm", + "packageManager": "yarn", "icon": "./public/images/favicon.ico" }, diff --git a/scripts/encrypt/README.md b/scripts/encrypt/README.md index 7e44116..b678338 100644 --- a/scripts/encrypt/README.md +++ b/scripts/encrypt/README.md @@ -83,8 +83,8 @@ 2. **运行加密脚本** ```bash - npm run encrypt:win # Windows - npm run encrypt:linux # Linux/Mac + yarncrypt:win # Windows + yarncrypt:linux # Linux/Mac ``` 3. **验证结果** diff --git a/scripts/encrypt/encrypt-electron.js b/scripts/encrypt/encrypt-electron.js index 8076e67..fe0b235 100644 --- a/scripts/encrypt/encrypt-electron.js +++ b/scripts/encrypt/encrypt-electron.js @@ -14,7 +14,7 @@ if (!process.versions.electron) { console.error('请使用以下方式运行:'); console.error(' Windows: encrypt-electron-win.bat'); console.error(' Linux/Mac: ./encrypt-electron.sh'); - console.error(' 或: npm run encrypt:win / npm run encrypt:linux'); + console.error(' 或: yarncrypt:win / yaryarnpt:linux'); process.exit(1); }