// design1核心功能模块 // 注意:此代码在layui.use回调函数内部执行,不需要$(function())包装 // 全局变量已在主入口文件中定义,这里只需要初始化 $("#object_attribute").css("height", "470px"); $("#obj_list").css("height", document.body.clientHeight - 520); $(".obj_list").css("height", $("#obj_list").height() - 32); $(".main").show(); $(".canvas_box").css("height", document.body.clientHeight - 100); zoom = $(".canvas_bg_img").width() / 2787 $("#canvas1").attr("width", $(".canvas_box").width() / zoom); $("#canvas1").attr("height", $(".canvas_box").height() / zoom); $("#canvas2").attr("width", $(".canvas_box").width() / zoom); $("#canvas2").attr("height", $(".canvas_box").height() / zoom); // 优化性能:在创建 Fabric Canvas 之前,先设置 canvas context 的 willReadFrequently // 注意:这需要在 Fabric.js 创建 canvas 之前执行 try { const canvas1El = document.getElementById('canvas1'); const canvas2El = document.getElementById('canvas2'); if (canvas1El) { // 先获取 context 并设置 willReadFrequently,这样 Fabric.js 创建时会复用这个 context canvas1El.getContext('2d', { willReadFrequently: true }); } if (canvas2El) { canvas2El.getContext('2d', { willReadFrequently: true }); } } catch (e) { // 如果浏览器不支持 willReadFrequently,忽略错误 } // 初始化Canvas对象 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._isApplyingHistory = false; // 同步到 window 对象 window.ctx1 = ctx1; window.ctx2 = ctx2; window.is_bgi_add = is_bgi_add; canvas1.zoomToPoint(new fabric.Point(0, 0), 1); canvas2.zoomToPoint(new fabric.Point(0, 0), 1); // 初始化标尺 initRulers(); $("#source_front").width(zoom * $("#source_front").width()); $("#source_back").width(zoom * $("#source_back").width()); $(".canvas-container:eq(0)").css('transform', 'scale(' + zoom + ')') $(".canvas-container:eq(1)").css('transform', 'scale(' + zoom + ')') $(".canvas-container:eq(0)").css('width', 0) $(".canvas-container:eq(1)").hide(); canvas = canvas1; window.canvas = canvas; fabric.Object.prototype.set({ borderColor: "#9013FE", cornerColor: "#9013FE", //激活状态角落图标的填充颜色 cornerStrokeColor: "#9013FE", //激活状态角落图标的边框颜色 borderOpacityWhenMoving: 1, borderScaleFactor: 6, cornerSize: 24, cornerStyle: "circle", //rect,circle centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 transparentCorners: false, //激活状态角落的图标是否透明 rotatingPointOffset: 20, //旋转距旋转体的距离 padding: 0, }); canvas1.preserveObjectStacking = true; canvas2.preserveObjectStacking = true; var front_list = []; function applyFontList(data) { var newFontList = []; (data || []).map(function(item) { if (item.indexOf('"') === 0) { newFontList.push(item.replace(/^"|"$/g, '')); } else { newFontList.push(item); } }); front_list = newFontList; for (var i = 0; i < front_list.length; i++) { var item = front_list[i]; $("#text_font_family").append(""); $("#circle_text_font_family").append(""); $("#out_text_font_family").append(""); $("#barcode_font_family").append(""); $("#count_font_family").append(""); } } if (typeof ipcRenderer !== 'undefined' && ipcRenderer) { ipcRenderer.on('font-list', function(event, data) { applyFontList(data); }); } else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) { window.platformBridge.getSystemFonts().then(applyFontList); } function applySysLan(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; } } if (typeof ipcRenderer !== 'undefined' && ipcRenderer) { ipcRenderer.on('sys-lan', function(event, data) { applySysLan(data); }); } else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getLocale) { applySysLan(window.platformBridge.getLocale()); } function onCloseConfirm() { 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(function() { if (ipcRenderer) ipcRenderer.send('run-close'); else if (window.platformBridge && window.platformBridge.runClose) window.platformBridge.runClose(); }); } }, function (index) { if (ipcRenderer) ipcRenderer.send('run-close'); else if (window.platformBridge && window.platformBridge.runClose) window.platformBridge.runClose(); }); } if (typeof ipcRenderer !== 'undefined' && ipcRenderer) { ipcRenderer.on('close', function(event, message) { onCloseConfirm(); }); } else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.onClose) { window.platformBridge.onClose(onCloseConfirm); } addBackground();//添加背景 白色卡片 initAligningGuidelines(canvas1); initAligningGuidelines(canvas2); //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();//重新渲染画布 // 立即启动事件初始化(必须在 slider 初始化之前) initCanvasEvents(); var zoomSlider = slider.render({ elem: '#zoomSlide', //绑定元素 min: 25, max: 500, value: 100, change(val) { canvas.zoomToPoint( new fabric.Point(0, 0), val / 100 ); // zoom = val / 100; //$(".canvas-container").css("transform","scale(" + val / 100 + ")") } }); // =========================================================== // Slider 滑块初始化 // =========================================================== var circle_text_size = slider.render({ elem: '#circle_text_size', min: 10, max: 120, change: function (value) { if (canvas.getActiveObject()) { canvas.getActiveObject().set("fontSize", value); canvas.renderAll(); } } }); var circle_text_diameter = slider.render({ elem: '#circle_text_diameter', min: 500, max: 4000, change: function (value) { if (canvas.getActiveObject()) { canvas.getActiveObject().set("diameter", value); canvas.renderAll(); } } }); var out_pic_op = slider.render({ elem: '#out_pic_op', change: function (value) { if (canvas.getActiveObject()) { canvas.getActiveObject().set('opacity', (100 - value) / 100); canvas.renderAll(); } } }); var pic_op = slider.render({ elem: '#pic_op', change: function (value) { if (canvas.getActiveObject()) { canvas.getActiveObject().set("opacity", (100 - value) / 100); canvas.renderAll(); } } }); var rect_op = slider.render({ elem: '#rect_op', change: function (value) { if (canvas.getActiveObject()) { canvas.getActiveObject().set("opacity", (100 - value) / 100); canvas.renderAll(); } } }); var circle_op = slider.render({ elem: '#circle_op', change: function (value) { if (canvas.getActiveObject()) { canvas.getActiveObject().set("opacity", (100 - value) / 100); canvas.renderAll(); } } }); window.rotate = function (id) { let angle = $(`#${id}`).attr("angle") - 0; angle += 90; $(`#${id}`).css("transform", `rotate(${angle}deg)`); $(`#${id}`).attr("angle", angle); } function addBackground() { // 类型改变 fabric.Image.fromURL(soonAsset('bg_front_1.png'), function (image) { let width = $("#canvas1").width() 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) { image.scale(1, 1); background_image1 = image; background_image = background_image1; window.background_image1 = background_image1; window.background_image = background_image; canvas.add(image); canvas.renderAll(); //pre_objs1.push(JSON.stringify(objs1)); //pre_json1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor"])); recordObjs1.push(JSON.stringify(objs1)); recordJson1.push(canvas1.toJSON(TO_JSON_PROPERTIES)); }); // 类型改变 fabric.Image.fromURL(soonAsset('bg_back.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(); //pre_objs2.push(JSON.stringify(objs2)); //pre_json2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor"])); recordObjs2.push(JSON.stringify(objs2)); recordJson2.push(canvas2.toJSON(TO_JSON_PROPERTIES)); let file = GetFile().get('file'); if (!file && typeof sessionStorage !== 'undefined') { try { file = sessionStorage.getItem('soondesign_open_file'); if (file) { sessionStorage.removeItem('soondesign_open_file'); sessionStorage.removeItem('soondesign_open_type'); } } catch (e) {} } if (file && file != 'empty') { if (file.indexOf('soondesign_file:') === 0 && typeof sessionStorage !== 'undefined') { try { var metaRaw = sessionStorage.getItem('soondesign_open_meta'); if (metaRaw) { window._soonFileMeta = JSON.parse(metaRaw); sessionStorage.removeItem('soondesign_open_meta'); } } catch (e) {} } var p = window.openFile(file) if (p && p.then) p.then(function() {}, function() {}) } }); }); is_bgi_add = 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", soonAsset("lock20.png"))//已锁定 } else { $("#lock_img").attr("src", soonAsset("unlock20.png"))//未锁定 } }; // 向后兼容 function updateControls() { return window.updateControls(); } function selectObject(target, index) { updateControls(); $("#base_control").show(); //$("#text_x").val((target.left - background_image.left).toFixed(1)); //$("#text_y").val((target.top - background_image.top).toFixed(1)); //$("#text_w").val(parseFloat(target.get("scaleX") * target.get("width")).toFixed(1)); //$("#text_h").val(parseFloat(target.get("scaleY") * target.get("height")).toFixed(1)); //$("#text_r").val(target.get("angle").toFixed(0)); } 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(); window.recordState(); //OpenDialog(); //function OpenDialog() //{ // dialog.showOpenDialog({ // title: "请选择图片", // buttonLabel: "确认", // filters: [ // { name: 'Image File Type', extensions: ['png', 'jpg'] }, // ] // }).then(result => { // if(result.canceled)return; // //console.log(result.filePaths) // fabric.Image.fromURL(result.filePaths[0], function(image) { // image.left = pointer.x; // image.top = pointer.y; // // canvas.add(image); // objs.push({type: 1}); // canvas.renderAll(); // updateList(); // window.recordState(); // } ); // }).catch(err => { // console.log(err) // }) //} } function selectPic(target, index) { //透明度 $("#pic_control").show(); $("#set_bg").show(); let activeObj = canvas.getActiveObject(); if (typeof pic_op !== 'undefined' && pic_op.setValue) { 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 + "%"); } let idx = getIndex(canvas.getActiveObject()); // $("#pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); $("#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 addOutPic(pointer) { fabric.Image.fromURL(soonAsset('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.on("selected",function(){//test well // console.log(222); //}); image.setSrc(image.toDataURL(), function (image) { canvas.add(image); objs.push({ type: 2, name: resName("OUT_PIC_"), black: false }); canvas.renderAll(); updateList(); window.recordState(); }); }); } 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()); $("#out_pic_field_name").val(objs[idx - 1].name); // $("#out_pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); $("#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 addText(pointer) { let text = new fabric.Text('TEXT', {//源:文本 left: pointer.x, top: pointer.y, fill: "#000000", textBackgroundColor: "", fontStyle: "normal", fontWeight: "normal", fontSize: 100, 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(); window.recordState(); } function selectText(target, index) { //console.log(canvas.getActiveObject().get("fontFamily")); $("#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 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: 100, underline: false, linethrough: false, lockScalingX: true, lockScalingY: true, fontFamily: 'Arial', originX: 'center', // originY: 'center', angle: angleDeg, flipped: pointer.y > canvas.height / 2 ? true : false }); // distance = getDisdance(pointer.x+text.width/2, pointer.y, canvas.width/2,canvas.height/2) // text.set({diameter: distance*2}) 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 }); canvas.renderAll(); updateList(); window.recordState(); } function selectCircleText() { // 检查是否有选中对象 if (!canvas || !canvas.getActiveObject()) return; let idx = getIndex(canvas.getActiveObject()); $("#circle_text_control").show(); $("#circle_text_color").val(canvas.getActiveObject().get("fill")); $("#circle_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); // 检查 circle_text_size 是否存在且有 setValue 方法 if (typeof circle_text_size !== 'undefined' && circle_text_size && typeof circle_text_size.setValue === 'function') { circle_text_size.setValue(canvas.getActiveObject().get("fontSize") - 10); } else { $("#circle_text_size").next().children(".layui-slider-bar").css("width", ((canvas.getActiveObject().get("fontSize") - 10 - 10) / (120 - 10)) * 100 + "%"); $("#circle_text_size").next().children(".layui-slider-handle").css("left", ((canvas.getActiveObject().get("fontSize") - 10 - 10) / (120 - 10)) * 100 + "%"); } // 检查 circle_text_diameter 是否存在且有 setValue 方法 if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter && typeof circle_text_diameter.setValue === 'function') { circle_text_diameter.setValue(canvas.getActiveObject().get("diameter") - 500); } else { $("#circle_text_diameter").next().children(".layui-slider-bar").css("width", ((canvas.getActiveObject().get("diameter") - 500 - 500) / (4000 - 500)) * 100 + "%"); $("#circle_text_diameter").next().children(".layui-slider-handle").css("left", ((canvas.getActiveObject().get("diameter") - 500 - 500) / (4000 - 500)) * 100 + "%"); } $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); $("#circle_text_text").val(canvas.getActiveObject().get("text")); $("#circle_text_font_family").val(canvas.getActiveObject().get("fontFamily")); if (canvas.getActiveObject().get("underline") == true) { $("#circle_text_underline").addClass("ui-radio-acitve") } else { $("#circle_text_underline").removeClass("ui-radio-acitve") } if (canvas.getActiveObject().get("fontWeight") == "bold") { $("#circle_text_bold").addClass("ui-radio-acitve") } else { $("#circle_text_bold").removeClass("ui-radio-acitve") } if (canvas.getActiveObject().get("fontStyle") == "italic") { $("#circle_text_italics").addClass("ui-radio-acitve") } else { $("#circle_text_italics").removeClass("ui-radio-acitve") } if (canvas.getActiveObject().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 addOutText(pointer) { //MERGE_TEXT_1 let text = new fabric.Text("MERGE TEXT", {//源:合成文本 left: pointer.x, top: pointer.y, fill: "#000000", fontStyle: "normal", fontWeight: "normal", fontSize: 100, textBackgroundColor: "", underline: false, linethrough: false, fontFamily: 'Arial', textAlign: 'center' }); //text.on({ // 'scaling': function(e) { // var obj = this, // w = obj.width * obj.scaleX, // h = obj.height * obj.scaleY; // // obj.set({ // 'height' : h, // 'width' : w, // 'scaleX' : 1, // 'scaleY' : 1 // }); // } //}); 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(); window.recordState(); } function selectOutText(target, index) { //color //size 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()); $("#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 addRect(pointer) { let rect = new fabric.Rect({ left: pointer.x, top: pointer.y, fill: '', stroke: "#ff0000", strokeWidth: 10, width: 300, height: 300 }); canvas.add(rect); objs.push({ type: 5, black: false }); canvas.renderAll(); updateList(); window.recordState(); } function selectRect(target, index) { //color $("#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 + "%"); } let a = canvas.getActiveObject().getCoords(); $("#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 addCircle(pointer) { let circle = new fabric.Circle({ left: pointer.x, top: pointer.y, fill: '', stroke: '#ff0000', strokeWidth: 10, radius: 150, strokeUniform: true, }); canvas.add(circle); objs.push({ type: 6, black: false }); canvas.renderAll(); updateList(); window.recordState(); } function selectCircle(target, index) { //border-color //color $("#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 addLine(pointer) { let line = new fabric.Line([100, 200, 400, 200], { left: pointer.x, top: pointer.y, stroke: '#ff0000', strokeWidth: 10, 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(); window.recordState(); } function selectLine(target, index) { //color //weight $("#line_control").show(); let idx = getIndex(canvas.getActiveObject()); $("#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 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; image.scale(2.4, 2.4) canvas.add(image); objs.push({ type: 8, name: resName("QR_CODE_"), color: "#000000", val: "https://www.cardsoon.com", black: false }); canvas.renderAll(); updateList(); window.recordState(); }); } function selectQrCode(target, index) { //字段名 //background-color //color $("#qrcode_Controll").show(); let idx = getIndex(canvas.getActiveObject()); $("#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 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; image.scale(2.4, 2.4) canvas.add(image); objs.push({ type: 9, name: resName("BAR_CODE_"), val: "123456789", color: "#000000", fontSize: 18, font: "Arial", show: false, black: false });//show是取消文字显示 canvas.renderAll(); updateList(); window.recordState(); }); } function selectBarCode(target, index) { //字段名 //background-color //color $("#barcode_controll").show(); let idx = getIndex(canvas.getActiveObject()); $("#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 addCounter(pointer) { let text = new fabric.Text('0', { left: pointer.x, top: pointer.y, fontSize: 100, fill: "#000000", }); canvas.add(text); objs.push({ type: 10, name: resName("COUNTER_"), start: 0, add: 1, black: false }); canvas.renderAll(); updateList(); window.recordState(); } function selectCounter(target, index) { //起始 //终止 //增量 $("#counter_control").show(); let idx = getIndex(canvas.getActiveObject()); $("#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(); } // 将 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 ? `` : ``; 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); } }); } // 将 selectObj 附加到 window 对象,确保全局可访问 // 标记是否通过对象列表选中(用于区分鼠标点击和列表点击) window._selectingFromList = false; 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(); } } } 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 = []; //next_objs = []; //recordObjs.push(JSON.stringify(objs)); //recordJson.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); ++step.val; for (let i = step.val; i < recordObjs.length; i++) { recordObjs.pop(); recordJson.pop(); } //recordObjs.splice(step.val, 1, JSON.stringify(objs)) //recordJson.splice(step.val, 1, canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])) recordObjs.push(JSON.stringify(objs)) j = canvas.toJSON(TO_JSON_PROPERTIES); j.objects[0].hoverCursor = "default"; recordJson.push(j); } // 将 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) { let temp_objs = canvas.getObjects(); let i = 0; for (let res of temp_objs) { if (target == res) { return objs[i].type; } i++; } } 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 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. 获取索引 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 数组下标 if (objs[i]) { 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; let idx = getIndex(obj); // 圆形文本特殊处理 if (idx > 0 && idx <= objs.length && objs[idx - 1] && 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(distance * 2 - 500); 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 属性列表,不包含锁定相关属性(锁定状态不应被保存) // 统一的 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 drawActiveObjectBorder(currentCanvas, ctx) { if (!currentCanvas || !ctx) return; const active = currentCanvas.getActiveObject(); if (!active) return; // 辅助线不需要额外描边 if (active.isGuideLine) return; const coords = active.getCoords(true); if (!coords || coords.length === 0) return; ctx.save(); ctx.beginPath(); ctx.strokeStyle = '#9013FE'; // 根据当前缩放比例调整线宽,避免看起来太虚 let lineWidth = 2; if (typeof zoom === 'number' && zoom > 0) { lineWidth = 2 / zoom; // 目标是屏幕上大约 2px 宽 } ctx.lineWidth = lineWidth; // 使用实线边框,提高清晰度 if (ctx.setLineDash) { ctx.setLineDash([]); } ctx.moveTo(coords[0].x, coords[0].y); for (let i = 1; i < coords.length; i++) { ctx.lineTo(coords[i].x, coords[i].y); } ctx.closePath(); ctx.stroke(); ctx.restore(); } 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)); } updateRulerGuides(e.e); }); // 2. 鼠标移出 currentCanvas.on("mouse:out", () => { $("#ruler-guide-h").hide(); $("#ruler-guide-v").hide(); }); // 3. 选中处理 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 false; // 完全排除辅助线 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(); } }); // 4. 鼠标松开 (添加对象) 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", ""); }); // 5. 对象移动 (只保留圆形文本逻辑,去掉了 checkOutOfBounds) currentCanvas.on("object:moving", (e) => { // 阻止背景图(索引0)被移动(如果已锁定) let movingObj = e.target; let idx = getIndex(movingObj); 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", () => { window.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; let idx = getIndex(event.target); if (idx <= 0) return; let objData = objs[idx - 1]; // 安全检查:确保 objData 存在且有 type 属性 if (!objData || typeof objData.type === 'undefined') { // 更新对象列表以同步锁状态 if (typeof window.updateList === 'function') { window.updateList(); } 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(); } window.recordState(); // 更新对象列表以同步锁状态 if (typeof window.updateList === 'function') { window.updateList(); } }); // ================================================== // 8. 【完美修复版】渲染后绘制:外部蒙版 + 中心圆孔 (修复三角形Bug) // ================================================== currentCanvas.on('after:render', function () { const ctx = currentCanvas.getContext(); let bgObj = (currentCanvas === canvas1) ? background_image1 : background_image2; if (bgObj) { const width = currentCanvas.width; const height = currentCanvas.height; // 1. 计算圆心 const cx = bgObj.left + (bgObj.width * bgObj.scaleX) / 2; const cy = bgObj.top + (bgObj.height * bgObj.scaleY) / 2; // 2. 计算半径 // 外半径 (光盘大小) const r_outer = (bgObj.width * bgObj.scaleX) / 2; // 内半径 (中心孔) const dpi = 600; const mmToPx = dpi / 25.4; let holeDiameterMm = 24; // 默认 15mm // 切换版本判断 (15mm vs 38mm) if (currentCanvas === canvas1 && typeof bg_version !== 'undefined' && bg_version === 2) { holeDiameterMm = 42; } // 比例计算法:确保孔径随缩放适配 // r_inner = r_outer * (孔径 / 光盘标准直径120mm) const r_inner = r_outer * (holeDiameterMm / 120); ctx.save(); ctx.beginPath(); // A. 绘制全屏矩形 (顺时针) ctx.rect(0, 0, width, height); // B. 挖掉外圆 (逆时针) // 【关键修复】画圆前,必须把笔移动到圆的起点 (cx + r, cy) // 否则 Canvas 会画一条线连过来,形成奇怪的三角形 ctx.moveTo(cx + r_outer, cy); ctx.arc(cx, cy, r_outer, 0, Math.PI * 2, true); // C. 填补内圆 (顺时针) // 【关键修复】同理,把笔移动到内圆的起点 ctx.moveTo(cx + r_inner, cy); ctx.arc(cx, cy, r_inner, 0, Math.PI * 2, false); ctx.closePath(); ctx.fillStyle = "rgba(0, 0, 0, 0.9)"; // 90% 遮罩 ctx.fill(); ctx.restore(); } // 在遮罩之后重新绘制辅助线,确保辅助线在最上层 const objects = currentCanvas.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; } // 使用 Fabric 对象的内置渲染方法 obj._render(ctx); ctx.restore(); } }); // 最后再补绘当前选中对象的边框,确保不会被遮罩层挡住 drawActiveObjectBorder(currentCanvas, ctx); }); }); } function initRulers() { const dpi = 600; const mmToPx = dpi / 25.4; const rulerHeight = 20; const rulerWidth = 20; const canvasWrapper = $('.canvas-wrapper'); const canvasBox = $('.canvas_box'); // 确保容器定位 if (canvasWrapper.css('position') === 'static') canvasWrapper.css('position', 'relative'); // 初始化尺寸 let canvasWidth = canvasWrapper.width() || 800; let canvasHeight = canvasWrapper.height() || 600; const $rulerH = $('#ruler-h'); const $rulerV = $('#ruler-v'); $rulerH.attr('width', canvasWidth).attr('height', rulerHeight); $rulerV.attr('width', rulerWidth).attr('height', canvasHeight); // --- 1. 绑定拖拽事件 (核心新功能) --- // 定义临时拖拽线 DOM let $dragLine = null; // 通用拖拽开始函数 const startDragGuide = (type, e) => { e.preventDefault(); e.stopPropagation(); // 防止重复调用:如果正在拖拽,直接返回 if ($dragLine && $dragLine.length > 0) { return; } // 创建临时显示的线 $dragLine = $('
'); $('.canvas-wrapper').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 $rulerH.off('mousedown').on('mousedown', (e) => startDragGuide('horizontal', e)); $rulerV.off('mousedown').on('mousedown', (e) => startDragGuide('vertical', e)); // --- 2. 在画布中创建辅助线 --- function createFabricGuide(type, domX, domY) { // domX 和 domY 是相对于 canvas-wrapper 的坐标 // 需要转换为相对于 canvas_box 的坐标,然后再转换为 Fabric 画布坐标 // 获取 canvas_box 相对于 canvas-wrapper 的位置 let boxPosition = canvasBox.position(); let boxMarginLeft = parseFloat(canvasBox.css('margin-left')) || 0; let boxMarginTop = parseFloat(canvasBox.css('margin-top')) || 0; // 计算相对于 canvas_box 的坐标(减去 box 在 wrapper 中的位置和 margin) let relX = domX - boxPosition.left - boxMarginLeft; let relY = domY - boxPosition.top - boxMarginTop; // 转为 Fabric 坐标 (除以 zoom) let canvasX = relX / zoom; let canvasY = relY / zoom; // 获取画布的实际尺寸(用于决定辅助线的长度) let canvasWidth = canvas.width || 2000; let canvasHeight = canvas.height || 2000; // 使用与画布尺寸相关的长度,避免“整块区域都像辅助线”,同时保证能横穿整个画布 let lineExtent = Math.max(canvasWidth, canvasHeight) * 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', // design1 中改回边框命中模型,保证更容易点中拖拽 perPixelTargetFind: false, targetFindTolerance: 10, padding: 10, 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.add(guideLine); canvas.setActiveObject(guideLine); canvas.renderAll(); } // --- 3. 绘制标尺 (核心绘制循环) --- function updateAndDraw() { // 每次重新获取尺寸,确保使用最新值 // 使用多种方法尝试获取实际尺寸,确保准确 let currentCanvasWidth = canvasWrapper.width() || canvasWrapper.outerWidth() || canvasWrapper[0].getBoundingClientRect().width || 800; let currentCanvasHeight = canvasWrapper.height() || canvasWrapper.outerHeight() || canvasWrapper[0].getBoundingClientRect().height || 600; // 如果还是获取不到,尝试使用父容器的尺寸 if (!currentCanvasWidth || currentCanvasWidth === 0) { const parent = canvasWrapper.parent(); currentCanvasWidth = parent.width() || parent[0].getBoundingClientRect().width || 800; } if (!currentCanvasHeight || currentCanvasHeight === 0) { const parent = canvasWrapper.parent(); currentCanvasHeight = parent.height() || parent[0].getBoundingClientRect().height || 600; } // 更新标尺尺寸 $rulerH.attr('width', currentCanvasWidth).attr('height', rulerHeight); $rulerV.attr('width', rulerWidth).attr('height', currentCanvasHeight); const pxPerMm = zoom * mmToPx; let boxMarginLeft = parseFloat(canvasBox.css('margin-left')) || 0; let boxMarginTop = parseFloat(canvasBox.css('margin-top')) || 0; const rulerHeightOffset = 20; // ruler 的高度 // 原点在标尺的左上角(画布的左上角位置,考虑margin) // 标尺0点 = boxMarginLeft(水平)和 boxMarginTop + rulerHeightOffset(垂直) let originX = boxMarginLeft; let originY = boxMarginTop + rulerHeightOffset; const ctxH = document.getElementById('ruler-h').getContext('2d', { willReadFrequently: true }); const ctxV = document.getElementById('ruler-v').getContext('2d', { willReadFrequently: true }); // 绘制水平标尺 function drawHorizontal() { ctxH.clearRect(0, 0, currentCanvasWidth, rulerHeight); ctxH.strokeStyle = '#666'; ctxH.lineWidth = 1; ctxH.beginPath(); ctxH.moveTo(0, 19); ctxH.lineTo(currentCanvasWidth, 19); ctxH.stroke(); ctxH.fillStyle = '#888'; ctxH.font = '9px Arial'; ctxH.textAlign = 'left'; ctxH.textBaseline = 'top'; const startX = 0; const endX = currentCanvasWidth; // 绘制主要刻度(每10mm) // 从0点向左绘制负值刻度,确保覆盖到startX for (let mm = -10; originX + mm * pxPerMm >= startX; mm -= 10) { const x = originX + mm * pxPerMm; ctxH.beginPath(); ctxH.moveTo(x, 5); ctxH.lineTo(x, 20); ctxH.stroke(); ctxH.fillText(mm + 'mm', x + 2, 2); } // 从0点向右绘制正值刻度,确保覆盖到endX for (let mm = 0; ; mm += 10) { const x = originX + mm * pxPerMm; if (x > endX) break; ctxH.beginPath(); ctxH.moveTo(x, 5); ctxH.lineTo(x, 20); ctxH.stroke(); ctxH.fillText(mm + 'mm', x + 2, 2); } // 绘制次要刻度(每1mm) // 从0点向左绘制负值刻度,确保覆盖到startX for (let mm = -1; originX + mm * pxPerMm >= startX; mm -= 1) { const x = originX + mm * pxPerMm; if (mm % 10 === 0) continue; ctxH.beginPath(); ctxH.moveTo(x, 12); ctxH.lineTo(x, 20); ctxH.stroke(); } // 从0点向右绘制正值刻度,确保覆盖到endX for (let mm = 0; ; mm += 1) { const x = originX + mm * pxPerMm; if (x > endX) break; if (mm % 10 === 0) continue; ctxH.beginPath(); ctxH.moveTo(x, 12); ctxH.lineTo(x, 20); ctxH.stroke(); } } // 绘制垂直标尺 function drawVertical() { ctxV.clearRect(0, 0, rulerWidth, currentCanvasHeight); ctxV.strokeStyle = '#666'; ctxV.lineWidth = 1; ctxV.beginPath(); ctxV.moveTo(19, 0); ctxV.lineTo(19, currentCanvasHeight); ctxV.stroke(); ctxV.fillStyle = '#888'; ctxV.font = '9px Arial'; ctxV.textAlign = 'center'; ctxV.textBaseline = 'middle'; const startY = 0; const endY = currentCanvasHeight; // 绘制主要刻度(每10mm) // 从0点向上绘制负值刻度,确保覆盖到startY for (let mm = -10; originY + mm * pxPerMm >= startY; mm -= 10) { const y = originY + mm * pxPerMm; ctxV.beginPath(); ctxV.moveTo(5, y); ctxV.lineTo(20, y); ctxV.stroke(); ctxV.save(); ctxV.translate(8, y + 2); ctxV.rotate(-Math.PI / 2); ctxV.fillText(mm + 'mm', 0, 0); ctxV.restore(); } // 从0点向下绘制正值刻度,确保覆盖到endY for (let mm = 0; ; mm += 10) { const y = originY + mm * pxPerMm; if (y > endY) break; ctxV.beginPath(); ctxV.moveTo(5, y); ctxV.lineTo(20, y); ctxV.stroke(); ctxV.save(); ctxV.translate(8, y + 2); ctxV.rotate(-Math.PI / 2); ctxV.fillText(mm + 'mm', 0, 0); ctxV.restore(); } // 绘制次要刻度(每1mm) // 从0点向上绘制负值刻度,确保覆盖到startY for (let mm = -1; originY + mm * pxPerMm >= startY; mm -= 1) { const y = originY + mm * pxPerMm; if (mm % 10 === 0) continue; ctxV.beginPath(); ctxV.moveTo(12, y); ctxV.lineTo(20, y); ctxV.stroke(); } // 从0点向下绘制正值刻度,确保覆盖到endY for (let mm = 0; ; mm += 1) { const y = originY + mm * pxPerMm; if (y > endY) break; if (mm % 10 === 0) continue; ctxV.beginPath(); ctxV.moveTo(12, y); ctxV.lineTo(20, y); ctxV.stroke(); } } drawHorizontal(); drawVertical(); } canvas1.on('after:render', updateAndDraw); canvas2.on('after:render', updateAndDraw); $(window).resize(() => setTimeout(updateAndDraw, 100)); updateAndDraw(); } // 辅助函数:更新光标位置 function updateRulerGuides(nativeEvent) { if (!nativeEvent) return; const wrapper = $('.canvas-wrapper'); const offset = wrapper.offset(); if (!offset) return; $("#ruler-guide-h").css({ left: (nativeEvent.pageX - offset.left) + 'px', display: 'block' }); $("#ruler-guide-v").css({ top: (nativeEvent.pageY - offset.top) + 'px', display: 'block' }); } window.copySelection = function copySelection() { const activeObjects = canvas.getActiveObjects(); if (!activeObjects || activeObjects.length === 0) { layer.msg(language_str("selectPic") || "请先选择对象"); return; } // 【核心修复】 // 遍历选中的对象,同时保存它的 Fabric 画面数据 和 我们的 objs 业务数据 clipboardData.data = activeObjects.map(obj => { let idx = getIndex(obj); // 获取对象在画布中的索引 // 获取业务属性 (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; } // 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. 刷新列表和历史记录 updateList(); window.recordState(); // 5. 高亮左侧列表最后一项 (即新粘贴的项) if (objs.length > 0) selectObj(objs.length - 1); // 6. 增加下次粘贴的偏移量 (让用户知道粘贴了新东西) clipboardData.offset += 10; }); } // 将 onStateRestored 附加到 window 对象,确保全局可访问 window.onStateRestored = function onStateRestored() { // 1. 重新获取背景图引用 (因为旧对象被销毁了) // 注意:canvas 是全局变量,指向当前激活的画板 if (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(); } }