From d4d325c2220b49772baf089389952bd11e50f796 Mon Sep 17 00:00:00 2001 From: Zhukj <3262118957> Date: Sat, 25 Apr 2026 11:57:26 +0800 Subject: [PATCH] =?UTF-8?q?merge:=20=E5=90=88=E5=B9=B6=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E5=88=86=E6=94=AF=20Branch=5Fmaster=20=E5=88=B0=20master?= =?UTF-8?q?=EF=BC=8C=E5=90=8C=E6=AD=A5=E6=89=80=E6=9C=89=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E8=BF=AD=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/style.css | 252 +++++++++++++++++++++++++-- index.html | 14 ++ js/qrcode.min.js | 1 + js/script.js | 432 +++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 613 insertions(+), 86 deletions(-) create mode 100644 js/qrcode.min.js diff --git a/css/style.css b/css/style.css index 5ae9fd4..546adc2 100644 --- a/css/style.css +++ b/css/style.css @@ -24,7 +24,7 @@ html, body { display: flex; justify-content: center; align-items: center; - gap: 6px; + gap: 4px; padding: 10px 12px; background: #fff; border-bottom: 1px solid #e0e0e0; @@ -32,10 +32,10 @@ html, body { } .btn { - min-width: 68px; - height: 36px; - padding: 0 10px; - font-size: 14px; + min-width: 56px; + height: 32px; + padding: 0 8px; + font-size: 13px; border: 1px solid #ddd; border-radius: 6px; background: #fff; @@ -53,7 +53,7 @@ html, body { } .btn-primary { - min-width: 100px; + min-width: 80px; background: #4a90e2; color: #fff; border-color: #4a90e2; @@ -66,18 +66,18 @@ html, body { /* 工具栏垂直分隔线 */ .toolbar-divider-v { width: 1px; - height: 20px; + height: 18px; background: #e0e0e0; - margin: 0 2px; + margin: 0 1px; } /* 工具栏图标按钮 */ .toolbar-btn-icon { - width: 28px; - height: 28px; - font-size: 14px; + width: 24px; + height: 24px; + font-size: 12px; border: 1px solid #ddd; - border-radius: 5px; + border-radius: 4px; background: #fff; color: #555; cursor: pointer; @@ -106,13 +106,13 @@ html, body { /* 撤销重做按钮组 */ .toolbar-action-group { display: flex; - gap: 3px; - margin: 0 2px; + gap: 2px; + margin: 0 1px; } /* 左侧分隔线(文本按钮后) */ .toolbar-divider-v.divider-left { - margin-left: 4px; + margin-left: 2px; margin-right: 0; } @@ -163,7 +163,7 @@ html, body { /* 编辑器工具栏(文本/图片共用) */ .editor-toolbar { position: absolute; - top: 65px; + top: 52px; left: 0; right: 0; display: flex; @@ -676,3 +676,223 @@ html, body { .grid-lines.visible { display: block; } + +/* ==================== 图层面板样式 ==================== */ +.layers-panel { + position: fixed; + top: 60px; + right: -280px; + width: 260px; + height: calc(100vh - 80px); + background: #fff; + border-left: 1px solid #e0e0e0; + box-shadow: -2px 0 10px rgba(0,0,0,0.1); + z-index: 200; + display: flex; + flex-direction: column; + transition: right 0.3s ease; +} + +.layers-panel.active { + right: 0; +} + +.layers-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid #e0e0e0; + background: #f8f8f8; +} + +.layers-title { + font-size: 14px; + font-weight: 600; + color: #333; +} + +.layers-close { + width: 24px; + height: 24px; + border: none; + background: transparent; + font-size: 18px; + color: #666; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s; +} + +.layers-close:hover { + background: #e0e0e0; + color: #333; +} + +.layers-list { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +.layer-item { + display: flex; + align-items: center; + padding: 10px 12px; + margin-bottom: 4px; + background: #f5f5f5; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + border: 2px solid transparent; +} + +.layer-item:hover { + background: #e8e8e8; +} + +.layer-item.selected { + background: #e3f2fd; + border-color: #4a90e2; +} + +.layer-item.hidden { + opacity: 0.5; + display: flex !important; +} + +.layer-icon { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; + font-size: 14px; +} + +.layer-info { + flex: 1; + min-width: 0; +} + +.layer-name { + font-size: 13px; + color: #333; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.layer-type { + font-size: 11px; + color: #999; + margin-top: 2px; +} + +.layer-visibility { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; + cursor: pointer; + padding: 4px; + border-radius: 4px; + transition: all 0.2s; + background: #4CAF50; + color: white; + border: 2px solid transparent; +} + +.layer-visibility.is-hidden { + background: #f44336; + color: white; +} + +.layer-visibility:hover { + opacity: 0.8; + transform: scale(1.1); +} + +.layer-visibility:active { + transform: scale(0.95); +} + +.layers-actions { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 12px; + border-top: 1px solid #e0e0e0; + background: #f8f8f8; + flex-wrap: wrap; +} + +.layers-btn { + width: 32px; + height: 32px; + border: 1px solid #ddd; + background: #fff; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.layers-btn:hover { + background: #f0f0f0; + border-color: #ccc; +} + +.layers-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.layers-btn-danger:hover { + background: #ffebee; + border-color: #ef5350; +} + +.layers-divider { + width: 1px; + height: 24px; + background: #ddd; + margin: 0 4px; +} + +/* 图层隐藏状态(用于画布上的元素) */ +.layer-hidden { + visibility: hidden !important; + pointer-events: none !important; +} + +/* 移动端适配 */ +@media (max-width: 768px) { + .layers-panel { + width: 240px; + right: -260px; + top: 50px; + height: calc(100vh - 50px); + } + + .layers-actions { + padding: 8px; + } + + .layers-btn { + width: 28px; + height: 28px; + font-size: 12px; + } +} diff --git a/index.html b/index.html index 388dcdb..6da74b6 100644 --- a/index.html +++ b/index.html @@ -19,6 +19,7 @@
+ @@ -117,6 +118,19 @@ + +
+
+ 图层 + +
+
+ +
+
+ +
+
diff --git a/js/qrcode.min.js b/js/qrcode.min.js new file mode 100644 index 0000000..2349ff2 --- /dev/null +++ b/js/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectionLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){this.dataList.push(new a(b))},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectionLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectionLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(d>>e&1);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(d>>e&1);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=f.getMaskFunction(b),i=this.moduleCount-1;i>0;i-=2)for(6==i&&i--;;){for(var j=0;2>j;j++)for(var k=this.modules[d][i-j];null==k;){if(g>>e&1;h(d,i-j)&&(l=!l),this.modules[d][i-j]=l,e--,g+=0>e&&(e=7,g++)}else this.modules[d][i-j]=!1}if(0>(d+=c)||this.moduleCount<=d){d-=c,c=-c;break}}}},b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;!(g.getLengthInBits()>=8*l);){if(g.put(236,8),g.getLengthInBits()>=8*l)break;g.put(17,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMaskFunction:function(a){switch(a){case e.PATTERN000:return function(a,b){return 0==(a+b)%2};case e.PATTERN001:return function(a,b){return 0==a%2};case e.PATTERN010:return function(a,b){return 0==b%3};case e.PATTERN011:return function(a,b){return 0==(a+b)%3};case e.PATTERN100:return function(a,b){return 0==(Math.floor(a/2)+Math.floor(b/3))%2};case e.PATTERN101:return function(a,b){return 0==a*b%2+a*b%3};case e.PATTERN110:return function(a,b){return 0==(a*b%2+a*b%3)%2};case e.PATTERN111:return function(a,b){return 0==(a*b%3+(a+b)%2)%2};default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push({totalCount:h,dataCount:i});return e},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(this.buffer[b]>>>7-a%8&1)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(a>>>b-c-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}},"string"==typeof a&&(a={text:a}),a=a||{},a=a||{},this.text=a.text||"",this.typeNumber=a.typeNumber||0,this.correctLevel=a.correctLevel||d.H,this.typeNumber<1&&(this.typeNumber=r(this.text,this.correctLevel)),this.makeImpl=function(a,c){for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectionLevel,this.dataList)),this.mapData(this.dataCache,c)},this.addData=function(a){this.dataList.push(new qrcode.QRCodeModel(a))},this.make=function(){this.makeImpl(!1,this.getBestMaskPattern())},this.createTableTag=function(a,b){a=a||2,b="undefined"==typeof b?4*a:b;var c="";c+='";for(var e=0;e';c+=""}return c+="",c+="
"},this.createImgTag=function(a,b){a=a||2,b="undefined"==typeof b?4*a:b;var c=this.createDataURL(a,b);return''},this.createSvgTag=function(a,b,c){a=a||2,b="undefined"==typeof b?4*a:b;var d=this.getModuleCount()*a+2*b,e="",f="\n";f+="\n",f+='\n',f+='\n';for(var g=0;g\n');return f+=e,f+="\n"},this.createDataURL=function(a,b){a=a||2,b="undefined"==typeof b?4*a:b;var c=this.getModuleCount()*a+2*b,d=b,e=c-b,f=document.createElement("canvas"),g=f.getContext("2d");f.width=f.height=c;for(var h=0;h\n";for(var g=0;gi;i++)f+=this.isDark(g,h)?"██":" ";f+="\n"}return f+=""},this.renderTo2dContext=function(a,b){b=b||2;for(var c=0;c { - const deleteBtn = document.createElement('button'); - deleteBtn.className = 'text-delete-btn'; - deleteBtn.innerHTML = '×'; - deleteBtn.addEventListener('click', (e) => { - e.stopPropagation(); - inputEl.remove(); - }); - inputEl.appendChild(deleteBtn); - }); - // 初始化图片滑动 initImageDrag(); @@ -448,6 +436,9 @@ // 初始化键盘微调功能 initKeyboardNudge(); + // 初始化图层管理 + initLayerManager(); + // 加载默认背景图 loadDefaultBackground(); @@ -542,6 +533,7 @@ overlays: Array.from(overlayLayer.querySelectorAll('.overlay-image')).map(el => { const img = el.querySelector('img'); return { + layerId: el.dataset.layerId, src: img ? img.src : '', left: el.style.left, top: el.style.top, @@ -549,7 +541,9 @@ height: el.style.height, opacity: el.style.opacity, transform: el.style.transform, - imgTransform: img ? img.style.transform : '' + imgTransform: img ? img.style.transform : '', + isHidden: el.classList.contains('layer-hidden'), + isLocked: el.classList.contains('locked') }; }), @@ -564,9 +558,13 @@ fontSize: el.style.fontSize, fontWeight: el.style.fontWeight, fontStyle: el.style.fontStyle, - isLocked: el.dataset.locked === 'true' + isLocked: el.dataset.locked === 'true', + isHidden: el.classList.contains('layer-hidden') })), + // 背景图隐藏状态 + bgImageHidden: bgImage.classList.contains('layer-hidden'), + // 添加时间戳用于状态管理 timestamp: Date.now() }; @@ -640,6 +638,13 @@ bgImageOpacity = state.bgImageOpacity; imageOffsetX = state.imageOffsetX; + // 恢复背景图隐藏状态 + if (state.bgImageHidden) { + bgImage.classList.add('layer-hidden'); + } else { + bgImage.classList.remove('layer-hidden'); + } + bgScaleSlider.value = Math.round(bgImageScale * 100); bgScaleValue.textContent = Math.round(bgImageScale * 100) + '%'; bgOpacitySlider.value = Math.round(bgImageOpacity * 100); @@ -658,6 +663,13 @@ bgImageOpacity = state.bgImageOpacity; imageOffsetX = state.imageOffsetX; + // 恢复背景图隐藏状态 + if (state.bgImageHidden) { + bgImage.classList.add('layer-hidden'); + } else { + bgImage.classList.remove('layer-hidden'); + } + bgScaleSlider.value = Math.round(bgImageScale * 100); bgScaleValue.textContent = Math.round(bgImageScale * 100) + '%'; bgOpacitySlider.value = Math.round(bgImageOpacity * 100); @@ -718,27 +730,8 @@ // 显示输入框 input.classList.remove('hidden'); - // 清除现有内容(保留删除按钮) - const deleteBtn = input.querySelector('.text-delete-btn'); - input.innerHTML = ''; - // 设置文本内容 - input.appendChild(document.createTextNode(inputData.content)); - - // 重新添加删除按钮 - if (deleteBtn) { - input.appendChild(deleteBtn); - } else { - const newDeleteBtn = document.createElement('button'); - newDeleteBtn.className = 'text-delete-btn'; - newDeleteBtn.innerHTML = '×'; - newDeleteBtn.addEventListener('click', (e) => { - e.stopPropagation(); - input.remove(); - saveState(); - }); - input.appendChild(newDeleteBtn); - } + input.textContent = inputData.content; // 应用样式 input.style.left = inputData.left; @@ -757,6 +750,13 @@ input.classList.remove('locked'); } + // 恢复隐藏状态 + if (inputData.isHidden) { + input.classList.add('layer-hidden'); + } else { + input.classList.remove('layer-hidden'); + } + // 检查是否需要显示 placeholder if (!inputData.content.trim()) { input.classList.add('show-placeholder'); @@ -863,6 +863,7 @@ const textEl = document.createElement('div'); textEl.className = 'text-input'; + textEl.id = 'text_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); textEl.contentEditable = 'false'; textEl.textContent = '新文本'; @@ -871,16 +872,6 @@ textEl.style.left = (left / containerWidth * 100) + '%'; textEl.style.top = (top / containerHeight * 100) + '%'; - // 删除按钮 - const deleteBtn = document.createElement('button'); - deleteBtn.className = 'text-delete-btn'; - deleteBtn.innerHTML = '×'; - deleteBtn.addEventListener('click', (e) => { - e.stopPropagation(); - textEl.remove(); - }); - textEl.appendChild(deleteBtn); - const inputLayer = container.querySelector('.preview-input-layer'); inputLayer.appendChild(textEl); @@ -929,6 +920,7 @@ // 创建层叠图元素 const overlayEl = document.createElement('div'); overlayEl.className = 'overlay-image'; + overlayEl.dataset.layerId = 'overlay_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); overlayEl.dataset.scale = 1; const imgEl = document.createElement('img'); @@ -938,18 +930,6 @@ imgEl.style.objectFit = 'contain'; overlayEl.appendChild(imgEl); - // 删除按钮 - const deleteBtn = document.createElement('button'); - deleteBtn.className = 'delete-btn'; - deleteBtn.innerHTML = '×'; - deleteBtn.addEventListener('click', (e) => { - e.stopPropagation(); - overlayEl.remove(); - // 保存状态(立即) - saveState(true); - }); - overlayEl.appendChild(deleteBtn); - // 居中显示 overlayEl.style.left = (containerWidth / 2 - width / 2) + 'px'; overlayEl.style.top = (containerHeight / 2 - height / 2) + 'px'; @@ -973,6 +953,7 @@ // 创建层叠图元素 const overlayEl = document.createElement('div'); overlayEl.className = 'overlay-image'; + overlayEl.dataset.layerId = overlayData.layerId || 'overlay_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); overlayEl.dataset.scale = 1; const imgEl = document.createElement('img'); @@ -983,17 +964,6 @@ imgEl.style.transform = overlayData.imgTransform || ''; overlayEl.appendChild(imgEl); - // 删除按钮 - const deleteBtn = document.createElement('button'); - deleteBtn.className = 'delete-btn'; - deleteBtn.innerHTML = '×'; - deleteBtn.addEventListener('click', (e) => { - e.stopPropagation(); - overlayEl.remove(); - saveState(true); - }); - overlayEl.appendChild(deleteBtn); - // 恢复位置和尺寸 overlayEl.style.left = overlayData.left; overlayEl.style.top = overlayData.top; @@ -1001,6 +971,16 @@ overlayEl.style.height = overlayData.height; overlayEl.style.opacity = overlayData.opacity || '1'; overlayEl.style.transform = overlayData.transform || ''; + + // 恢复隐藏状态 + if (overlayData.isHidden) { + overlayEl.classList.add('layer-hidden'); + } + + // 恢复锁定状态 + if (overlayData.isLocked) { + overlayEl.classList.add('locked'); + } // 初始化拖拽和缩放 initOverlayImage(overlayEl); @@ -2416,6 +2396,309 @@ }, { passive: false }); } + // ==================== 图层管理系统 ==================== + let selectedLayerId = null; + + function initLayerManager() { + const layersBtn = document.getElementById('layersBtn'); + const layersPanel = document.getElementById('layersPanel'); + const layersClose = document.getElementById('layersClose'); + const layersList = document.getElementById('layersList'); + + if (!layersBtn || !layersPanel) return; + + // 打开/关闭图层面板(同时支持桌面和移动端) + const toggleLayersPanel = () => { + layersPanel.classList.toggle('active'); + updateLayersList(); + }; + layersBtn.addEventListener('click', toggleLayersPanel); + layersBtn.addEventListener('touchend', (e) => { e.preventDefault(); toggleLayersPanel(); }); + + const closeLayersPanel = () => { + layersPanel.classList.remove('active'); + }; + layersClose.addEventListener('click', closeLayersPanel); + layersClose.addEventListener('touchend', (e) => { e.preventDefault(); closeLayersPanel(); }); + + // 点击空白区域关闭图层面板 + const handleClickOutside = (e) => { + // 如果点击的是图层面板内部,不关闭 + if (layersPanel.contains(e.target) || layersBtn.contains(e.target)) { + return; + } + // 关闭图层面板 + layersPanel.classList.remove('active'); + }; + + // 桌面端使用 click 事件 + document.addEventListener('click', handleClickOutside); + // 移动端使用 touchend 事件 + document.addEventListener('touchend', (e) => { + if (layersPanel.classList.contains('active')) { + handleClickOutside(e); + } + }); + + // 图层操作按钮 - 只保留显示/隐藏(同时支持桌面和移动端) + const layerToggleVisibleBtn = document.getElementById('layerToggleVisible'); + if (layerToggleVisibleBtn) { + const handleToggleVisible = (e) => { + e.stopPropagation(); + e.preventDefault(); + toggleLayerVisibility(); + }; + layerToggleVisibleBtn.addEventListener('click', handleToggleVisible); + layerToggleVisibleBtn.addEventListener('touchend', handleToggleVisible); + } + + // 点击图层列表项选中图层(同时支持桌面和移动端) + let isProcessingLayerClick = false; + const handleLayerListClick = (e) => { + // 防止重复触发 + if (isProcessingLayerClick) return; + isProcessingLayerClick = true; + setTimeout(() => { isProcessingLayerClick = false; }, 100); + + console.log('图层列表点击', e.target, e.target.className); + + // 处理眼睛图标点击 - 直接切换可见性 + const visibilityBtn = e.target.closest('.layer-visibility'); + if (visibilityBtn) { + console.log('点击眼睛图标', visibilityBtn.dataset.layerId); + e.stopPropagation(); + e.preventDefault(); + const layerId = visibilityBtn.dataset.layerId; + const element = getLayerElement(layerId); + console.log('获取元素', element, '当前layer-hidden:', element?.classList.contains('layer-hidden')); + if (element) { + const newHiddenState = !element.classList.contains('layer-hidden'); + console.log('切换隐藏状态为:', newHiddenState); + if (newHiddenState) { + element.classList.add('layer-hidden'); + } else { + element.classList.remove('layer-hidden'); + } + console.log('切换后layer-hidden:', element.classList.contains('layer-hidden')); + updateLayersList(); + saveState(true); + } + return; + } + + const layerItem = e.target.closest('.layer-item'); + if (!layerItem) return; + + const layerId = layerItem.dataset.layerId; + selectLayer(layerId); + }; + + // 只在桌面端使用 click 事件 + if (!('ontouchstart' in window)) { + layersList.addEventListener('click', handleLayerListClick); + } else { + // 移动端使用 touchend 事件 + layersList.addEventListener('touchend', handleLayerListClick); + } + } + + // 更新图层列表 + function updateLayersList() { + const layersList = document.getElementById('layersList'); + if (!layersList) return; + + // 保存当前滚动位置 + const scrollTop = layersList.scrollTop; + + // 使用 requestAnimationFrame 确保在下一帧更新,避免事件冲突 + requestAnimationFrame(() => { + layersList.innerHTML = ''; + + // 获取所有图层(按指定顺序:背景图-企业-嘉宾-编号-其他文本-层叠图) + const layers = []; + + // 1. 背景图(最底层) + if (currentImage) { + layers.push({ + id: 'background', + element: bgImage, + name: '背景图', + type: 'background', + icon: '🌄', + visible: !bgImage.classList.contains('layer-hidden') + }); + } + + // 2. 固定文本图层(按顺序:企业、嘉宾、编号) + const fixedOrder = ['input1', 'input2', 'input3']; + fixedOrder.forEach(id => { + const input = document.getElementById(id); + if (input) { + layers.push({ + id: input.id, + element: input, + name: getLayerName(input), + type: 'text', + icon: '📝', + visible: !input.classList.contains('layer-hidden') + }); + } + }); + + // 3. 其他文本输入框(新添加的) + const textInputs = document.querySelectorAll('.preview-input-layer .text-input'); + textInputs.forEach((input) => { + // 跳过已处理的固定图层 + if (!fixedOrder.includes(input.id)) { + layers.push({ + id: input.id, + element: input, + name: getLayerName(input), + type: 'text', + icon: '📝', + visible: !input.classList.contains('layer-hidden') + }); + } + }); + + // 4. 层叠图(最上层) + const overlayImages = overlayLayer.querySelectorAll('.overlay-image'); + overlayImages.forEach((overlay, index) => { + layers.push({ + id: overlay.dataset.layerId || `overlay_${index}`, + element: overlay, + name: `层叠图 ${index + 1}`, + type: 'overlay', + icon: '🖼️', + visible: !overlay.classList.contains('layer-hidden') + }); + }); + + // 显示图层列表(已经是正确顺序,不需要倒序) + console.log('updateLayersList: 总共', layers.length, '个图层'); + layers.forEach((layer, index) => { + console.log('updateLayersList: 创建图层项', index, layer.id, layer.name, 'visible:', layer.visible); + const layerItem = document.createElement('div'); + layerItem.className = 'layer-item'; + layerItem.dataset.layerId = layer.id; + if (layer.id === selectedLayerId) { + layerItem.classList.add('selected'); + } + if (!layer.visible) { + layerItem.classList.add('hidden'); + } + + layerItem.innerHTML = ` +
${layer.icon}
+
+
${layer.name}
+
${getLayerTypeName(layer.type)}
+
+
+ ${layer.visible ? '✓' : '✕'} +
+ `; + + layersList.appendChild(layerItem); + console.log('updateLayersList: 已添加到列表', layer.id); + }); + + // 恢复滚动位置 + layersList.scrollTop = scrollTop; + + // 更新按钮状态 + updateLayerButtons(); + }); + } + + // 获取图层名称 + function getLayerName(element) { + console.log('getLayerName', element.id, element.textContent); + const text = element.textContent.trim(); + if (text) { + return text.length > 10 ? text.substring(0, 10) + '...' : text; + } + const placeholder = element.dataset.placeholder; + const id = element.id; + if (id === 'input1') return '企业'; + if (id === 'input2') return '嘉宾'; + if (id === 'input3') return '编号'; + return placeholder || '文本'; + } + + // 获取图层类型名称 + function getLayerTypeName(type) { + const names = { + 'text': '文本', + 'overlay': '层叠图', + 'background': '背景图' + }; + return names[type] || type; + } + + // 选中图层 + function selectLayer(layerId) { + selectedLayerId = layerId; + + // 取消所有选中 + document.querySelectorAll('.layer-item').forEach(item => { + item.classList.remove('selected'); + }); + + // 高亮当前选中的图层项 + const layerItem = document.querySelector(`.layer-item[data-layer-id="${layerId}"]`); + if (layerItem) { + layerItem.classList.add('selected'); + } + + // 选中对应的元素 + const element = getLayerElement(layerId); + if (element) { + if (element.classList.contains('text-input')) { + selectInput(element); + } else if (element.classList.contains('overlay-image')) { + selectOverlay(element); + } else if (element.id === 'bgImage') { + // 选中背景图 + editorToolbar.classList.add('active'); + textTools.classList.remove('active'); + imageTools.classList.remove('active'); + bgTools.classList.add('active'); + } + } + + updateLayerButtons(); + } + + // 获取图层元素 + function getLayerElement(layerId) { + if (layerId === 'background') return bgImage; + return document.getElementById(layerId) || document.querySelector(`[data-layer-id="${layerId}"]`); + } + + // 更新图层按钮状态 + function updateLayerButtons() { + const hasSelection = selectedLayerId !== null; + + // 显示/隐藏按钮需要选中图层才能使用 + const visibleBtn = document.getElementById('layerToggleVisible'); + if (visibleBtn) { + visibleBtn.disabled = !hasSelection; + } + } + + // 切换图层可见性 + function toggleLayerVisibility() { + if (!selectedLayerId) return; + + const element = getLayerElement(selectedLayerId); + if (!element) return; + + element.classList.toggle('layer-hidden'); + updateLayersList(); + saveState(true); + } + // 初始化键盘微调功能(方向键 1px 移动) function initKeyboardNudge() { document.addEventListener('keydown', (e) => { @@ -2846,8 +3129,8 @@ const scale = TARGET_WIDTH / displayWidth; console.log('统一缩放比例:', scale, '画布宽高比:', TARGET_WIDTH/TARGET_HEIGHT, '容器宽高比:', displayRatio); - // 绘制背景图 - if (currentImage) { + // 绘制背景图(检查是否被隐藏) + if (currentImage && !bgImage.classList.contains('layer-hidden')) { if (document.getElementById('globalLoading')) { loadingManager.updateMessage('正在绘制背景...'); } @@ -2884,7 +3167,10 @@ ]); textInputs.forEach(textInput => { - drawTextOnCanvasHD(ctx, textInput, scale); + // 跳过隐藏的文本图层 + if (!textInput.classList.contains('layer-hidden')) { + drawTextOnCanvasHD(ctx, textInput, scale); + } }); // 记录总耗时 @@ -3044,6 +3330,12 @@ for (const batch of batches) { await Promise.all(batch.map(async (overlayEl) => { + // 跳过隐藏的图层 + if (overlayEl.classList.contains('layer-hidden')) { + processedCount++; + return; + } + const imgEl = overlayEl.querySelector('img'); if (!imgEl) { processedCount++;