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++;