diff --git a/css/style.css b/css/style.css index 5ae9fd4..a84c976 100644 --- a/css/style.css +++ b/css/style.css @@ -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/script.js b/js/script.js index 532af3c..68d1728 100644 --- a/js/script.js +++ b/js/script.js @@ -268,18 +268,6 @@ initInput(input2); initInput(input3); - // 为三个输入框添加删除按钮 - [input1, input2, input3].forEach(inputEl => { - 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,274 @@ }, { 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 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. 文本输入框(最上层) + const textInputs = document.querySelectorAll('.preview-input-layer .text-input'); + console.log('updateLayersList: 找到', textInputs.length, '个文本输入框'); + textInputs.forEach((input, index) => { + console.log('updateLayersList: 处理文本输入框', input.id, 'layer-hidden:', input.classList.contains('layer-hidden')); + // 只过滤掉预设的隐藏状态,显示所有图层(包括被隐藏的) + layers.push({ + id: input.id, + element: input, + name: getLayerName(input), + type: 'text', + icon: '📝', + visible: !input.classList.contains('layer-hidden') + }); + }); + + // 2. 层叠图 + 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') + }); + }); + + // 3. 背景图(最底层) + if (currentImage) { + layers.push({ + id: 'background', + element: bgImage, + name: '背景图', + type: 'background', + icon: '🌄', + visible: !bgImage.classList.contains('layer-hidden') + }); + } + + // 倒序显示(顶层在上) + console.log('updateLayersList: 总共', layers.length, '个图层'); + layers.reverse().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 +3094,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 +3132,10 @@ ]); textInputs.forEach(textInput => { - drawTextOnCanvasHD(ctx, textInput, scale); + // 跳过隐藏的文本图层 + if (!textInput.classList.contains('layer-hidden')) { + drawTextOnCanvasHD(ctx, textInput, scale); + } }); // 记录总耗时 @@ -3044,6 +3295,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++;