Compare commits
1 Commits
Branch_mas
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4d325c222 |
252
css/style.css
252
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;
|
||||
}
|
||||
}
|
||||
|
||||
14
index.html
14
index.html
@@ -19,6 +19,7 @@
|
||||
<button class="toolbar-btn-icon" id="redoBtn" title="重做 (Ctrl+Y)">↪</button>
|
||||
</div>
|
||||
<div class="toolbar-divider-v divider-right"></div>
|
||||
<button class="btn" id="layersBtn">图层</button>
|
||||
<button class="btn btn-primary" id="saveBtn">保存/预览</button>
|
||||
</div>
|
||||
|
||||
@@ -117,6 +118,19 @@
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 图层面板 -->
|
||||
<div class="layers-panel" id="layersPanel">
|
||||
<div class="layers-header">
|
||||
<span class="layers-title">图层</span>
|
||||
<button class="layers-close" id="layersClose">×</button>
|
||||
</div>
|
||||
<div class="layers-list" id="layersList">
|
||||
<!-- 图层项将动态生成 -->
|
||||
</div>
|
||||
<div class="layers-actions">
|
||||
<button class="layers-btn" id="layerToggleVisible" title="显示/隐藏">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="file" id="fileInput" accept="image/*" hidden>
|
||||
|
||||
1
js/qrcode.min.js
vendored
Normal file
1
js/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
432
js/script.js
432
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;
|
||||
@@ -1002,6 +972,16 @@
|
||||
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 = `
|
||||
<div class="layer-icon">${layer.icon}</div>
|
||||
<div class="layer-info">
|
||||
<div class="layer-name">${layer.name}</div>
|
||||
<div class="layer-type">${getLayerTypeName(layer.type)}</div>
|
||||
</div>
|
||||
<div class="layer-visibility ${!layer.visible ? 'is-hidden' : ''}" title="${layer.visible ? '点击隐藏图层' : '点击显示图层'}" data-layer-id="${layer.id}" data-visible="${layer.visible}">
|
||||
${layer.visible ? '✓' : '✕'}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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++;
|
||||
|
||||
Reference in New Issue
Block a user