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+='',c+="";for(var d=0;d";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"},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++;