4 Commits

4 changed files with 174 additions and 61 deletions

10
app.py
View File

@@ -1,7 +1,7 @@
""" """
图片编辑器 - Image Editor v1.2.2 图片编辑器 - Image Editor v1.2.6
前端图片处理工具:合并、分割、挖孔填充、圆形切图、文字图片等 前端图片处理工具:合并、分割、挖孔填充、圆形切图、文字图片等
v1.2.2: 右侧面板始终显示,首页显示快捷入口 v1.2.6: 操作后显示预览效果,保留原始图片
端口: 19018 端口: 19018
""" """
@@ -25,7 +25,7 @@ def index():
@app.route('/api/health') @app.route('/api/health')
def health(): def health():
return jsonify({'status': 'ok', 'version': '1.2.2', 'time': datetime.now().isoformat()}) return jsonify({'status': 'ok', 'version': '1.2.6', 'time': datetime.now().isoformat()})
@app.route('/api/save', methods=['POST']) @app.route('/api/save', methods=['POST'])
def save_image(): def save_image():
@@ -64,9 +64,9 @@ def list_images():
if __name__ == '__main__': if __name__ == '__main__':
print("=" * 50) print("=" * 50)
print("图片编辑器 - Image Editor v1.2.2") print("图片编辑器 - Image Editor v1.2.6")
print("=" * 50) print("=" * 50)
print("右侧面板始终显示,首页显示快捷入口") print("操作后显示预览效果,保留原始图片")
print(f"访问地址: http://localhost:19018") print(f"访问地址: http://localhost:19018")
print("=" * 50) print("=" * 50)
app.run(host='0.0.0.0', port=19018, debug=True) app.run(host='0.0.0.0', port=19018, debug=True)

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 KiB

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片编辑器 - Image Editor v1.2.2</title> <title>图片编辑器 - Image Editor v1.2.6</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style> <style>
@@ -90,9 +90,6 @@
<!-- 功能按钮 --> <!-- 功能按钮 -->
<div class="mt-3 flex gap-2 flex-wrap"> <div class="mt-3 flex gap-2 flex-wrap">
<button onclick="setTool('upload')" id="tool-upload" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-upload-cloud-line mr-1"></i>上传图片
</button>
<button onclick="setTool('merge')" id="tool-merge" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg"> <button onclick="setTool('merge')" id="tool-merge" class="tool-btn px-3 py-2 bg-gray-200 rounded-lg">
<i class="ri-stack-line mr-1"></i>合并图片 <i class="ri-stack-line mr-1"></i>合并图片
</button> </button>
@@ -127,7 +124,8 @@
<canvas id="mainCanvas"></canvas> <canvas id="mainCanvas"></canvas>
<div id="emptyHint" class="text-gray-400 text-center"> <div id="emptyHint" class="text-gray-400 text-center">
<i class="ri-image-add-line text-6xl"></i> <i class="ri-image-add-line text-6xl"></i>
<p class="mt-2">点击"上传图片"或拖放图片开始编辑</p> <p class="mt-2">拖动图片到此处上传</p>
<p class="text-sm mt-1">支持多张图片同时上传</p>
</div> </div>
</div> </div>
@@ -185,18 +183,150 @@
selection: null, selection: null,
textStyles: { bold: false, italic: false }, textStyles: { bold: false, italic: false },
mergeImageOrder: [], mergeImageOrder: [],
mergeImageSizes: {} mergeImageSizes: {},
previewMode: false, // 预览模式
previewData: null, // 预览图片数据
originalImages: null // 预览前保存的原始图片
}; };
const canvas = document.getElementById('mainCanvas'); const canvas = document.getElementById('mainCanvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const container = document.getElementById('canvasContainer'); const container = document.getElementById('canvasContainer');
// 返回首页面板 // 检查是否有图片(画布有内容)
function goHome() { function hasImage() {
document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active')); return state.images.length > 0 || document.getElementById('emptyHint').classList.contains('hidden');
state.currentTool = null; }
showPanel('home'); // 显示预览效果(操作后显示结果,但保留原始图片)
function showPreviewResult(dataUrl, name) {
// 进入预览模式
state.previewMode = true;
state.previewData = dataUrl;
// 保存原始图片状态
state.originalImages = state.images.map(img => ({
...img,
// 不复制img对象只保存数据
}));
// 显示预览效果(追加结果图片,不清除原始)
const img = new Image();
img.onload = () => {
// 调整画布大小以适应结果图片
canvas.width = img.width;
canvas.height = img.height;
// 绘制结果图片
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
hideEmptyHint();
// 显示预览面板
showPreviewPanel(name);
};
img.src = dataUrl;
}
// 显示预览面板
function showPreviewPanel(operationName) {
document.getElementById('panelTitle').textContent = '预览效果';
document.getElementById('panelContent').innerHTML = `
<div class="space-y-3">
<div class="p-3 bg-green-50 rounded-lg">
<p class="text-sm text-green-700">
<i class="ri-check-line"></i> ${operationName} 完成!
</p>
<p class="text-xs text-green-600 mt-1">预览效果已显示,原图片仍保留</p>
</div>
<p class="text-xs text-gray-500">选择"应用结果"替换原图,或"取消"恢复</p>
</div>
`;
document.getElementById('panelActions').innerHTML = `
<button onclick="applyPreviewResult()" class="px-4 py-2 bg-green-500 text-white rounded-lg w-full mb-2">
<i class="ri-check-double-line mr-1"></i>应用结果
</button>
<button onclick="cancelPreview()" class="px-4 py-2 bg-gray-200 rounded-lg w-full mb-2">
<i class="ri-close-line mr-1"></i>取消预览
</button>
<button onclick="goHome()" class="px-4 py-2 bg-blue-100 text-blue-600 rounded-lg w-full">
<i class="ri-home-line mr-1"></i>返回首页
</button>
`;
}
// 应用预览结果
function applyPreviewResult() {
if (!state.previewData) return;
// 清空原始图片,只保留结果
state.images = [];
const img = new Image();
img.onload = () => {
state.images.push({
img: img,
name: 'result_' + Date.now(),
x: 0,
y: 0,
width: img.width,
height: img.height,
dataUrl: state.previewData
});
drawCanvas();
updateImageList();
saveState('应用结果');
// 清除预览状态
state.previewMode = false;
state.previewData = null;
state.originalImages = null;
// 返回首页
goHome();
};
img.src = state.previewData;
}
// 取消预览
function cancelPreview() {
if (!state.originalImages) {
goHome();
return;
}
// 恢复原始图片
state.images = [];
state.originalImages.forEach((item, idx) => {
if (state.images[idx] && state.images[idx].img) {
state.images.push(state.images[idx]);
}
});
// 如果没有恢复成功,重新从画布历史恢复
if (state.images.length === 0 && state.historyStack.length > 0) {
const lastState = state.historyStack[state.historyStack.length - 1];
restoreState(lastState);
} else {
drawCanvas();
updateImageList();
}
// 清除预览状态
state.previewMode = false;
state.previewData = null;
state.originalImages = null;
// 返回首页
goHome();
}
// 刷新当前工具面板(执行操作后保持当前配置)
function refreshCurrentPanel() {
if (state.currentTool) {
showPanel(state.currentTool);
}
} }
// 初始化 // 初始化
@@ -238,11 +368,6 @@
// 更新右侧配置面板内容(不隐藏) // 更新右侧配置面板内容(不隐藏)
showPanel(tool); showPanel(tool);
// 上传图片直接触发文件选择
if (tool === 'upload') {
document.getElementById('fileInput').click();
}
} }
// 显示右侧面板内容 // 显示右侧面板内容
@@ -259,18 +384,18 @@
// 获取各工具的配置面板内容 // 获取各工具的配置面板内容
function getPanelConfig(tool) { function getPanelConfig(tool) {
// 默认首页面板 <!-- 默认首页面板 -->
if (!tool || tool === 'home') { if (!tool || tool === 'home') {
return { return {
title: '欢迎使用', title: '欢迎使用',
content: ` content: `
<div class="space-y-4"> <div class="space-y-4">
<p class="text-sm text-gray-600">点击顶部功能按钮开始编辑图片</p> <p class="text-sm text-gray-600">点击顶部功能按钮开始编辑图片</p>
<div class="p-3 bg-blue-50 rounded-lg border-2 border-dashed border-blue-300 text-center">
<i class="ri-upload-cloud-2-line text-3xl text-blue-500"></i>
<p class="text-sm text-blue-600 mt-2">拖动图片到左侧画布区域上传</p>
</div>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<button onclick="setTool('upload')" class="p-3 bg-blue-50 rounded-lg hover:bg-blue-100 text-center">
<i class="ri-upload-cloud-line text-2xl text-blue-500"></i>
<p class="text-xs mt-1">上传图片</p>
</button>
<button onclick="setTool('merge')" class="p-3 bg-green-50 rounded-lg hover:bg-green-100 text-center"> <button onclick="setTool('merge')" class="p-3 bg-green-50 rounded-lg hover:bg-green-100 text-center">
<i class="ri-stack-line text-2xl text-green-500"></i> <i class="ri-stack-line text-2xl text-green-500"></i>
<p class="text-xs mt-1">合并图片</p> <p class="text-xs mt-1">合并图片</p>
@@ -311,20 +436,9 @@
} }
switch(tool) { switch(tool) {
case 'upload':
return {
title: '上传图片',
content: `
<p class="text-sm text-gray-600 mb-4">点击按钮选择图片,或直接拖放图片到画布区域</p>
<button onclick="document.getElementById('fileInput').click()" class="w-full px-4 py-3 bg-blue-500 text-white rounded-lg">
<i class="ri-upload-cloud-line mr-2"></i>选择图片
</button>
`,
actions: ''
};
case 'merge': case 'merge':
if (state.images.length < 2) { if (!hasImage() || state.images.length < 2) {
return { return {
title: '合并图片', title: '合并图片',
content: `<p class="text-sm text-red-500">请先上传至少2张图片</p>`, content: `<p class="text-sm text-red-500">请先上传至少2张图片</p>`,
@@ -401,7 +515,7 @@
}; };
case 'split': case 'split':
if (state.images.length === 0) { if (!hasImage()) {
return { return {
title: '分割图片', title: '分割图片',
content: `<p class="text-sm text-red-500">请先上传图片</p>`, content: `<p class="text-sm text-red-500">请先上传图片</p>`,
@@ -432,7 +546,7 @@
}; };
case 'hole': case 'hole':
if (state.images.length === 0) { if (!hasImage()) {
return { return {
title: '挖孔填充', title: '挖孔填充',
content: `<p class="text-sm text-red-500">请先上传图片</p>`, content: `<p class="text-sm text-red-500">请先上传图片</p>`,
@@ -478,7 +592,7 @@
}; };
case 'circle': case 'circle':
if (state.images.length === 0) { if (!hasImage()) {
return { return {
title: '圆形切图', title: '圆形切图',
content: `<p class="text-sm text-red-500">请先上传图片</p>`, content: `<p class="text-sm text-red-500">请先上传图片</p>`,
@@ -564,7 +678,7 @@
}; };
case 'resize': case 'resize':
if (state.images.length === 0) { if (!hasImage()) {
return { return {
title: '调整大小', title: '调整大小',
content: `<p class="text-sm text-red-500">请先上传图片</p>`, content: `<p class="text-sm text-red-500">请先上传图片</p>`,
@@ -595,7 +709,7 @@
}; };
case 'crop': case 'crop':
if (state.images.length === 0) { if (!hasImage()) {
return { return {
title: '裁剪', title: '裁剪',
content: `<p class="text-sm text-red-500">请先上传图片</p>`, content: `<p class="text-sm text-red-500">请先上传图片</p>`,
@@ -865,10 +979,17 @@
updateImageList(); updateImageList();
hideEmptyHint(); hideEmptyHint();
saveState('加载: ' + name); saveState('加载: ' + name);
// 刷新当前面板
refreshCurrentPanel();
}; };
img.src = dataUrl; img.src = dataUrl;
} }
// 加载操作结果(不清空原始图片)
function loadOperationResult(dataUrl, name, operationName) {
showPreviewResult(dataUrl, operationName);
}
function drawCanvas() { function drawCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#ffffff'; ctx.fillStyle = '#ffffff';
@@ -889,7 +1010,7 @@
const list = document.getElementById('imageList'); const list = document.getElementById('imageList');
const items = document.getElementById('imageItems'); const items = document.getElementById('imageItems');
document.getElementById('imageCount').textContent = `(${state.images.length}张)`; document.getElementById('imageCount').textContent = `(${state.images.length}张)`;
if (state.images.length === 0) { list.classList.add('hidden'); return; } if (!hasImage()) { list.classList.add('hidden'); return; }
list.classList.remove('hidden'); list.classList.remove('hidden');
items.innerHTML = ''; items.innerHTML = '';
state.images.forEach((item, index) => { state.images.forEach((item, index) => {
@@ -909,7 +1030,7 @@
drawCanvas(); drawCanvas();
updateImageList(); updateImageList();
saveState('移除图片'); saveState('移除图片');
if (state.images.length === 0) showEmptyHint(); if (!hasImage()) showEmptyHint();
} }
function hideEmptyHint() { function hideEmptyHint() {
@@ -970,6 +1091,7 @@
canvas.width = cols * maxWidth + (cols - 1) * gap; canvas.width = cols * maxWidth + (cols - 1) * gap;
canvas.height = rows * maxHeight + (rows - 1) * gap; canvas.height = rows * maxHeight + (rows - 1) * gap;
// 绘制合并结果到画布(预览)
ctx.fillStyle = bgColor; ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
@@ -989,10 +1111,9 @@
} }
}); });
// 显示预览效果(不清除原始图片)
const mergedData = canvas.toDataURL('image/png'); const mergedData = canvas.toDataURL('image/png');
state.images = []; showPreviewResult(mergedData, '合并图片');
loadImage(mergedData, 'merged');
goHome();
} }
function applySplit() { function applySplit() {
@@ -1029,7 +1150,7 @@
} }
document.getElementById('splitResults').classList.remove('hidden'); document.getElementById('splitResults').classList.remove('hidden');
goHome(); refreshCurrentPanel();
saveState(`分割 (${cols}x${rows})`); saveState(`分割 (${cols}x${rows})`);
} }
@@ -1168,9 +1289,7 @@
ctx.drawImage(circleCanvas, 0, 0); ctx.drawImage(circleCanvas, 0, 0);
const circleData = canvas.toDataURL('image/png'); const circleData = canvas.toDataURL('image/png');
state.images = []; showPreviewResult(circleData, '圆形切图');
loadImage(circleData, 'circle');
goHome();
} }
function createTextImage() { function createTextImage() {
@@ -1219,9 +1338,7 @@
}); });
const textData = canvas.toDataURL('image/png'); const textData = canvas.toDataURL('image/png');
state.images = []; showPreviewResult(textData, '文字图片');
loadImage(textData, 'text');
goHome();
} }
function applyResize() { function applyResize() {
@@ -1237,9 +1354,7 @@
ctx.drawImage(img.img, 0, 0, newWidth, newHeight); ctx.drawImage(img.img, 0, 0, newWidth, newHeight);
const resizedData = canvas.toDataURL('image/png'); const resizedData = canvas.toDataURL('image/png');
state.images = []; showPreviewResult(resizedData, '调整大小');
loadImage(resizedData, 'resized');
goHome();
} }
function applyCrop() { function applyCrop() {
@@ -1257,9 +1372,7 @@
ctx.drawImage(img.img, x, y, w, h, 0, 0, w, h); ctx.drawImage(img.img, x, y, w, h, 0, 0, w, h);
const croppedData = canvas.toDataURL('image/png'); const croppedData = canvas.toDataURL('image/png');
state.images = []; showPreviewResult(croppedData, '裁剪');
loadImage(croppedData, 'cropped');
goHome();
} }
function saveImage() { function saveImage() {