merge: 合并开发分支 Branch_master 到 master,同步所有功能迭代

This commit is contained in:
Zhukj
2026-04-25 11:57:26 +08:00
parent ef6010a8b2
commit d4d325c222
4 changed files with 613 additions and 86 deletions

View File

@@ -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;
}
}

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

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