Qwen-Image-Edit前端集成:JavaScript实现实时编辑预览

1. 为什么需要在浏览器里直接编辑图片

你有没有遇到过这样的场景:设计同事发来一张海报,需要把上面的活动时间从"8月15日"改成"9月20日",但你手边没有Photoshop,临时装个专业软件又太麻烦?或者电商运营要批量修改几十张商品图的文字信息,每张都得打开编辑软件、找字体、调大小、对齐位置,一上午就过去了。

Qwen-Image-Edit这个模型真正解决了这类问题——它能让AI理解你的自然语言指令,直接在图片上做精准修改。但光有强大的后端模型还不够,如果每次都要上传图片、等待API响应、再下载结果,体验就大打折扣了。真正的效率提升,应该发生在用户点击"修改"按钮的那一刻,就能看到实时预览效果。

这就是我们今天要做的:用纯JavaScript把Qwen-Image-Edit集成到网页中,让用户拖拽一张图片,输入一句"把标题改成'秋季特惠',字体变大加粗",就能立刻看到修改后的效果。整个过程不需要跳转页面,不依赖复杂环境,打开浏览器就能用。

整个方案的核心思路很朴素:前端负责友好的交互和即时反馈,后端提供稳定可靠的图像处理能力,两者通过标准API无缝协作。下面我们就一步步实现这个目标。

2. 前端环境准备与基础架构

2.1 创建最小可行项目结构

首先,我们创建一个极简的HTML页面作为起点。不需要任何构建工具或框架,纯原生JavaScript就能搞定:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Qwen-Image-Edit在线编辑器</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: "Segoe UI", system-ui, sans-serif; line-height: 1.6; color: #333; background: #f8f9fa; }
        .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
        .editor-section { display: flex; gap: 20px; margin-bottom: 30px; flex-wrap: wrap; }
        .panel { flex: 1; min-width: 300px; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); overflow: hidden; }
        .panel-header { padding: 16px 20px; background: #4a6fa5; color: white; font-weight: 600; }
        .panel-body { padding: 20px; }
        .drop-area { border: 2px dashed #4a6fa5; border-radius: 6px; padding: 40px 20px; text-align: center; cursor: pointer; transition: all 0.3s; }
        .drop-area:hover, .drop-area.dragover { background: #eef3ff; border-color: #2c5282; }
        .btn { background: #4a6fa5; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 500; }
        .btn:hover { background: #2c5282; }
        .btn:disabled { background: #a0aec0; cursor: not-allowed; }
        .image-preview { max-width: 100%; height: auto; border-radius: 4px; margin-top: 15px; }
        .input-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 6px; font-weight: 500; }
        input, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
        textarea { min-height: 100px; resize: vertical; }
        .result-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
        .result-item { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
        .result-header { padding: 12px 16px; background: #f1f5f9; font-size: 14px; color: #4a5568; }
        .result-content { padding: 16px; }
        .loading { text-align: center; padding: 20px; color: #4a5568; }
        .spinner { border: 3px solid #f3f3f3; border-top: 3px solid #4a6fa5; border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; margin: 0 auto 10px; }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
    </style>
</head>
<body>
    <div class="container">
        <h1>Qwen-Image-Edit在线编辑器</h1>
        <p>用自然语言指令编辑图片,无需安装任何软件</p>
        
        <div class="editor-section">
            <div class="panel">
                <div class="panel-header">原始图片</div>
                <div class="panel-body">
                    <div class="drop-area" id="dropArea">
                        <p> 拖拽图片到这里</p>
                        <p>或点击选择文件</p>
                        <input type="file" id="fileInput" accept="image/*" style="display: none;">
                        <button class="btn" id="browseBtn">选择图片</button>
                    </div>
                    <img id="originalImage" class="image-preview" style="display: none;" alt="原始图片">
                </div>
            </div>
            
            <div class="panel">
                <div class="panel-header">编辑设置</div>
                <div class="panel-body">
                    <div class="input-group">
                        <label for="promptInput">编辑指令</label>
                        <textarea id="promptInput" placeholder="例如:把标题文字改为'新品上市',背景换成蓝色渐变,添加公司logo水印">把标题文字改为'秋季特惠',字体变大加粗,背景换成浅灰色</textarea>
                    </div>
                    <div class="input-group">
                        <label for="outputSize">输出尺寸</label>
                        <select id="outputSize">
                            <option value="1024x1024">1024×1024(正方形)</option>
                            <option value="1024x1536">1024×1536(竖版)</option>
                            <option value="1536x1024">1536×1024(横版)</option>
                            <option value="original">保持原图尺寸</option>
                        </select>
                    </div>
                    <button class="btn" id="editBtn">开始编辑</button>
                </div>
            </div>
        </div>
        
        <div class="panel">
            <div class="panel-header">编辑结果</div>
            <div class="panel-body">
                <div id="resultContainer" class="result-grid">
                    <div class="result-item">
                        <div class="result-header">处理中...</div>
                        <div class="result-content">
                            <div class="loading">
                                <div class="spinner"></div>
                                <p>正在调用AI模型处理图片,请稍候</p>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
        // 我们将在下面逐步添加JavaScript代码
    </script>
</body>
</html>

这个HTML文件已经包含了完整的UI结构:左侧是图片上传区域,中间是编辑指令输入框,右侧是结果展示区。所有样式都是内联的,确保复制粘贴就能运行,不需要额外的CSS文件。

2.2 图片上传与预览功能实现

接下来,我们为图片上传区域添加交互逻辑。这段代码会处理用户拖拽图片、点击选择、以及图片预览:

// 获取DOM元素
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const browseBtn = document.getElementById('browseBtn');
const originalImage = document.getElementById('originalImage');
const promptInput = document.getElementById('promptInput');
const editBtn = document.getElementById('editBtn');
const resultContainer = document.getElementById('resultContainer');

// 处理文件选择
function handleFileSelect(file) {
    if (!file || !file.type.match('image.*')) {
        alert('请选择一张图片文件');
        return;
    }
    
    const reader = new FileReader();
    reader.onload = function(e) {
        originalImage.src = e.target.result;
        originalImage.style.display = 'block';
        // 保存原始图片数据供后续使用
        window.originalImageData = {
            file: file,
            url: e.target.result,
            name: file.name
        };
    };
    reader.readAsDataURL(file);
}

// 拖拽事件处理
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
    dropArea.addEventListener(eventName, preventDefaults, false);
});

function preventDefaults(e) {
    e.preventDefault();
    e.stopPropagation();
}

['dragenter', 'dragover'].forEach(eventName => {
    dropArea.addEventListener(eventName, highlight, false);
});

['dragleave', 'drop'].forEach(eventName => {
    dropArea.addEventListener(eventName, unhighlight, false);
});

function highlight() {
    dropArea.classList.add('dragover');
}

function unhighlight() {
    dropArea.classList.remove('dragover');
}

// 处理拖拽放置
dropArea.addEventListener('drop', handleDrop, false);

function handleDrop(e) {
    const dt = e.dataTransfer;
    const files = dt.files;
    if (files.length) {
        handleFileSelect(files[0]);
    }
}

// 处理点击选择
browseBtn.addEventListener('click', () => {
    fileInput.click();
});

fileInput.addEventListener('change', (e) => {
    if (e.target.files.length) {
        handleFileSelect(e.target.files[0]);
    }
});

// 初始化时设置默认提示词
promptInput.value = "把标题文字改为'秋季特惠',字体变大加粗,背景换成浅灰色";

这段代码实现了完整的图片上传流程:

  • 支持拖拽图片到指定区域
  • 支持点击按钮选择文件
  • 实时预览上传的图片
  • 自动保存图片元数据供后续处理

现在你可以保存这个HTML文件,在浏览器中打开,拖拽一张图片进去,就能看到即时预览效果了。

3. JavaScript与Qwen-Image-Edit API的对接

3.1 API调用封装与错误处理

Qwen-Image-Edit提供了标准的REST API接口,我们需要用JavaScript封装一个安全可靠的调用方法。这里的关键是处理各种可能的错误情况,给用户提供清晰的反馈:

// API配置 - 这里使用阿里云百炼平台的API端点
const API_CONFIG = {
    baseUrl: 'https://dashscope.aliyuncs.com/api/v1',
    model: 'qwen-image-edit-plus',
    timeout: 300000 // 5分钟超时,因为图像处理需要时间
};

// API调用函数
async function callQwenImageEditAPI(imageData, prompt, size = '1024x1024') {
    // 验证必要参数
    if (!imageData || !imageData.url || !prompt.trim()) {
        throw new Error('缺少必要的参数:图片或编辑指令');
    }
    
    // 构建请求体
    const requestBody = {
        model: API_CONFIG.model,
        input: {
            messages: [
                {
                    role: "user",
                    content: [
                        { "image": imageData.url },
                        { "text": prompt }
                    ]
                }
            ]
        },
        parameters: {
            n: 1,
            watermark: false,
            negative_prompt: "",
            prompt_extend: true,
            size: size
        }
    };
    
    try {
        // 检查API密钥是否已配置
        const apiKey = getApiKey();
        if (!apiKey) {
            throw new Error('未配置API密钥,请在控制台设置DASHSCOPE_API_KEY环境变量,或在代码中配置');
        }
        
        const response = await fetch(`${API_CONFIG.baseUrl}/services/aigc/multimodal-generation/generation`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${apiKey}`
            },
            body: JSON.stringify(requestBody),
            signal: AbortSignal.timeout(API_CONFIG.timeout)
        });
        
        if (!response.ok) {
            const errorData = await response.json();
            let errorMessage = `API请求失败:${response.status} ${response.statusText}`;
            if (errorData.message) {
                errorMessage += ` - ${errorData.message}`;
            }
            throw new Error(errorMessage);
        }
        
        const result = await response.json();
        
        // 验证响应格式
        if (!result.output || !result.output.choices || result.output.choices.length === 0) {
            throw new Error('API响应格式异常:缺少有效输出');
        }
        
        const imageUrls = [];
        const content = result.output.choices[0].message.content;
        if (Array.isArray(content)) {
            content.forEach(item => {
                if (item.image && typeof item.image === 'string') {
                    imageUrls.push(item.image);
                }
            });
        }
        
        if (imageUrls.length === 0) {
            throw new Error('API返回了空的图片URL列表');
        }
        
        return {
            success: true,
            imageUrls: imageUrls,
            requestId: result.request_id || 'unknown',
            usage: result.usage || {}
        };
        
    } catch (error) {
        // 处理不同类型的错误
        if (error.name === 'AbortError') {
            throw new Error('请求超时:图像处理时间过长,请尝试简化编辑指令或降低输出尺寸');
        }
        if (error.message.includes('401')) {
            throw new Error('认证失败:请检查API密钥是否正确且未过期');
        }
        if (error.message.includes('403')) {
            throw new Error('权限不足:当前API密钥没有调用Qwen-Image-Edit的权限');
        }
        if (error.message.includes('429')) {
            throw new Error('请求过于频繁:请稍后再试,或检查API配额');
        }
        throw error;
    }
}

// 获取API密钥的辅助函数
function getApiKey() {
    // 首先尝试从环境变量获取(Node.js环境)
    if (typeof process !== 'undefined' && process.env) {
        return process.env.DASHSCOPE_API_KEY;
    }
    
    // 浏览器环境中,可以从localStorage或全局变量获取
    // 注意:生产环境中不应在前端存储敏感密钥,这里仅为演示
    const apiKey = localStorage.getItem('DASHSCOPE_API_KEY') || 
                   window.DASHSCOPE_API_KEY ||
                   '';
    
    return apiKey;
}

// 设置API密钥的辅助函数
function setApiKey(key) {
    if (key && key.trim()) {
        localStorage.setItem('DASHSCOPE_API_KEY', key.trim());
        return true;
    }
    return false;
}

这个API封装函数包含了企业级应用所需的关键特性:

  • 完整的错误处理和用户友好的错误消息
  • 请求超时控制,避免界面长时间无响应
  • 参数验证,防止无效请求
  • 响应格式验证,确保数据完整性
  • 灵活的API密钥管理方式

3.2 前端API密钥管理方案

在生产环境中,直接在前端暴露API密钥存在安全风险。但为了教学演示的目的,我们提供一个安全的密钥管理方案:

// 在页面加载完成后初始化密钥管理
document.addEventListener('DOMContentLoaded', () => {
    // 检查是否有已保存的密钥
    const savedKey = localStorage.getItem('DASHSCOPE_API_KEY');
    if (savedKey) {
        document.getElementById('apiKeyStatus').textContent = ' API密钥已配置';
        document.getElementById('apiKeyStatus').style.color = '#38a169';
    } else {
        document.getElementById('apiKeyStatus').textContent = ' 请先配置API密钥';
        document.getElementById('apiKeyStatus').style.color = '#e53e3e';
    }
});

// 添加API密钥配置弹窗
function showApiKeyModal() {
    const modalHtml = `
        <div id="apiKeyModal" style="
            position: fixed; top: 0; left: 0; width: 100%; height: 100%; 
            background: rgba(0,0,0,0.5); display: flex; align-items: center; 
            justify-content: center; z-index: 1000;
        ">
            <div style="
                background: white; padding: 24px; border-radius: 8px; 
                width: 90%; max-width: 500px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            ">
                <h3 style="margin-bottom: 16px;">配置API密钥</h3>
                <p style="margin-bottom: 16px; color: #4a5568;">
                    请从阿里云百炼平台获取您的API密钥,然后粘贴到下方输入框中。
                </p>
                <div style="margin-bottom: 16px;">
                    <label style="display: block; margin-bottom: 6px; font-weight: 500;">API密钥</label>
                    <input type="password" id="apiKeyInput" 
                           style="width: 100%; padding: 10px; border: 1px solid #ddd; 
                                  border-radius: 4px; font-size: 14px;" 
                           placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
                </div>
                <div style="display: flex; gap: 10px; justify-content: flex-end;">
                    <button onclick="closeApiKeyModal()" 
                            style="padding: 8px 16px; background: #e2e8f0; border: none; 
                                   border-radius: 4px; cursor: pointer;">取消</button>
                    <button onclick="saveApiKey()" 
                            style="padding: 8px 16px; background: #4a6fa5; color: white; 
                                   border: none; border-radius: 4px; cursor: pointer;">保存</button>
                </div>
                <p style="margin-top: 16px; font-size: 12px; color: #718096;">
                     提示:API密钥仅保存在您当前浏览器的本地存储中,不会发送到任何服务器。
                </p>
            </div>
        </div>
    `;
    
    document.body.insertAdjacentHTML('beforeend', modalHtml);
}

function closeApiKeyModal() {
    const modal = document.getElementById('apiKeyModal');
    if (modal) modal.remove();
}

function saveApiKey() {
    const input = document.getElementById('apiKeyInput');
    const key = input.value.trim();
    
    if (!key) {
        alert('API密钥不能为空');
        return;
    }
    
    if (setApiKey(key)) {
        alert('API密钥保存成功!');
        document.getElementById('apiKeyStatus').textContent = ' API密钥已配置';
        document.getElementById('apiKeyStatus').style.color = '#38a169';
        closeApiKeyModal();
    } else {
        alert('保存API密钥失败,请重试');
    }
}

// 在编辑按钮点击事件中添加密钥检查
editBtn.addEventListener('click', async () => {
    const apiKey = getApiKey();
    if (!apiKey) {
        showApiKeyModal();
        return;
    }
    
    // 继续执行编辑逻辑...
});

这个密钥管理方案既保证了教学演示的可行性,又提醒了用户生产环境中的安全最佳实践。

4. 实时编辑预览功能实现

4.1 编辑流程状态管理

为了让用户清楚地了解当前处理状态,我们需要实现一个完整的工作流状态管理系统:

// 编辑状态枚举
const EDIT_STATUS = {
    IDLE: 'idle',
    UPLOADING: 'uploading',
    PROCESSING: 'processing',
    SUCCESS: 'success',
    ERROR: 'error'
};

// 当前编辑状态
let currentStatus = EDIT_STATUS.IDLE;

// 状态更新函数
function updateStatus(status, message = '') {
    currentStatus = status;
    
    // 更新UI状态
    switch(status) {
        case EDIT_STATUS.IDLE:
            editBtn.disabled = false;
            editBtn.textContent = '开始编辑';
            break;
        case EDIT_STATUS.UPLOADING:
            editBtn.disabled = true;
            editBtn.textContent = '上传中...';
            break;
        case EDIT_STATUS.PROCESSING:
            editBtn.disabled = true;
            editBtn.textContent = '处理中...';
            break;
        case EDIT_STATUS.SUCCESS:
            editBtn.disabled = false;
            editBtn.textContent = '编辑完成';
            break;
        case EDIT_STATUS.ERROR:
            editBtn.disabled = false;
            editBtn.textContent = '重新编辑';
            break;
    }
    
    // 显示状态消息
    if (message) {
        showStatusMessage(message, status === EDIT_STATUS.ERROR ? 'error' : 'info');
    }
}

// 显示状态消息的辅助函数
function showStatusMessage(message, type = 'info') {
    // 创建临时消息元素
    const messageEl = document.createElement('div');
    messageEl.className = 'status-message';
    messageEl.style.cssText = `
        position: fixed; top: 20px; right: 20px; 
        padding: 12px 20px; border-radius: 4px; 
        color: white; font-size: 14px; 
        z-index: 1001; 
        transform: translateX(120%);
        transition: transform 0.3s ease-out;
    `;
    
    if (type === 'error') {
        messageEl.style.background = '#e53e3e';
        messageEl.style.boxShadow = '0 2px 8px rgba(229, 62, 62, 0.3)';
    } else {
        messageEl.style.background = '#4a6fa5';
        messageEl.style.boxShadow = '0 2px 8px rgba(74, 111, 165, 0.3)';
    }
    
    messageEl.textContent = message;
    document.body.appendChild(messageEl);
    
    // 动画显示
    setTimeout(() => {
        messageEl.style.transform = 'translateX(0)';
    }, 10);
    
    // 自动隐藏
    setTimeout(() => {
        messageEl.style.transform = 'translateX(120%)';
        setTimeout(() => {
            if (messageEl.parentNode) {
                messageEl.parentNode.removeChild(messageEl);
            }
        }, 300);
    }, 3000);
}

这个状态管理系统为整个编辑流程提供了清晰的用户反馈,避免了用户在等待时的焦虑感。

4.2 核心编辑功能实现

现在我们把所有组件组合起来,实现核心的编辑功能:

// 主编辑函数
async function performImageEdit() {
    // 验证前置条件
    if (currentStatus !== EDIT_STATUS.IDLE) return;
    
    if (!window.originalImageData) {
        updateStatus(EDIT_STATUS.ERROR, '请先上传一张图片');
        return;
    }
    
    const prompt = promptInput.value.trim();
    if (!prompt) {
        updateStatus(EDIT_STATUS.ERROR, '请输入编辑指令');
        return;
    }
    
    const sizeSelect = document.getElementById('outputSize');
    const outputSize = sizeSelect.value === 'original' 
        ? `${window.originalImageData.file.width || 1024}x${window.originalImageData.file.height || 1024}` 
        : sizeSelect.value;
    
    try {
        updateStatus(EDIT_STATUS.PROCESSING, '正在调用AI模型处理图片...');
        
        // 调用API
        const result = await callQwenImageEditAPI(
            window.originalImageData, 
            prompt, 
            outputSize
        );
        
        // 处理成功结果
        updateStatus(EDIT_STATUS.SUCCESS, '图片编辑完成!');
        
        // 渲染结果
        renderResults(result);
        
    } catch (error) {
        console.error('编辑失败:', error);
        updateStatus(EDIT_STATUS.ERROR, error.message || '图片编辑失败,请重试');
    }
}

// 渲染结果的函数
function renderResults(result) {
    // 清空结果容器
    resultContainer.innerHTML = '';
    
    // 显示原始图片对比
    const originalItem = document.createElement('div');
    originalItem.className = 'result-item';
    originalItem.innerHTML = `
        <div class="result-header">原始图片</div>
        <div class="result-content">
            <img src="${window.originalImageData.url}" 
                 alt="原始图片" 
                 class="image-preview"
                 style="max-width: 100%; border-radius: 4px;">
        </div>
    `;
    resultContainer.appendChild(originalItem);
    
    // 显示编辑结果
    result.imageUrls.forEach((url, index) => {
        const resultItem = document.createElement('div');
        resultItem.className = 'result-item';
        resultItem.innerHTML = `
            <div class="result-header">编辑结果 #${index + 1}</div>
            <div class="result-content">
                <img src="${url}" 
                     alt="编辑结果" 
                     class="image-preview"
                     style="max-width: 100%; border-radius: 4px;">
                <div style="margin-top: 12px; display: flex; gap: 8px;">
                    <button class="btn" style="flex: 1; padding: 6px 12px; font-size: 12px;"
                            onclick="downloadImage('${url}', 'edited_image_${index + 1}.png')">下载</button>
                    <button class="btn" style="flex: 1; padding: 6px 12px; font-size: 12px; background: #38a169;"
                            onclick="compareWithOriginal('${url}')">对比</button>
                </div>
            </div>
        `;
        resultContainer.appendChild(resultItem);
    });
    
    // 添加使用统计信息
    const statsItem = document.createElement('div');
    statsItem.className = 'result-item';
    statsItem.innerHTML = `
        <div class="result-header">处理信息</div>
        <div class="result-content">
            <p><strong>请求ID:</strong>${result.requestId}</p>
            <p><strong>处理时间:</strong>${new Date().toLocaleTimeString()}</p>
            <p><strong>输出尺寸:</strong>${result.usage.width || '1024'}×${result.usage.height || '1024'}</p>
            <p><strong>图片数量:</strong>${result.imageUrls.length}</p>
        </div>
    `;
    resultContainer.appendChild(statsItem);
}

// 下载图片的辅助函数
function downloadImage(url, filename) {
    fetch(url)
        .then(response => response.blob())
        .then(blob => {
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = filename;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        })
        .catch(error => {
            console.error('下载失败:', error);
            showStatusMessage('图片下载失败,请重试', 'error');
        });
}

// 对比功能
function compareWithOriginal(editedUrl) {
    // 创建对比视图
    const compareHtml = `
        <div id="compareModal" style="
            position: fixed; top: 0; left: 0; width: 100%; height: 100%; 
            background: rgba(0,0,0,0.8); display: flex; align-items: center; 
            justify-content: center; z-index: 1000; padding: 20px;
        ">
            <div style="
                background: white; border-radius: 8px; width: 100%; max-width: 1200px; 
                max-height: 90vh; overflow: hidden; display: flex; flex-direction: column;
            ">
                <div style="
                    padding: 16px 20px; background: #4a6fa5; color: white; 
                    display: flex; justify-content: space-between; align-items: center;
                ">
                    <h3>图片对比视图</h3>
                    <button onclick="closeCompareModal()" 
                            style="background: none; border: none; color: white; font-size: 18px; cursor: pointer;">✕</button>
                </div>
                <div style="display: flex; flex: 1; overflow: auto;">
                    <div style="flex: 1; padding: 20px; border-right: 1px solid #eee;">
                        <h4 style="margin-bottom: 12px; color: #4a6fa5;">原始图片</h4>
                        <img src="${window.originalImageData.url}" 
                             alt="原始图片" 
                             style="max-width: 100%; max-height: 60vh; object-fit: contain;">
                    </div>
                    <div style="flex: 1; padding: 20px;">
                        <h4 style="margin-bottom: 12px; color: #4a6fa5;">编辑结果</h4>
                        <img src="${editedUrl}" 
                             alt="编辑结果" 
                             style="max-width: 100%; max-height: 60vh; object-fit: contain;">
                    </div>
                </div>
                <div style="padding: 16px 20px; background: #f8f9fa; text-align: center;">
                    <p style="color: #4a5568; margin: 0;">拖动图片可查看细节,滚动鼠标可缩放</p>
                </div>
            </div>
        </div>
    `;
    
    document.body.insertAdjacentHTML('beforeend', compareHtml);
    
    // 添加图片缩放功能
    const images = document.querySelectorAll('#compareModal img');
    images.forEach(img => {
        img.style.cursor = 'zoom-in';
        img.addEventListener('click', function() {
            this.style.transform = this.style.transform === 'scale(2)' ? 'scale(1)' : 'scale(2)';
            this.style.transition = 'transform 0.3s ease';
        });
    });
}

function closeCompareModal() {
    const modal = document.getElementById('compareModal');
    if (modal) modal.remove();
}

// 绑定事件监听器
editBtn.addEventListener('click', performImageEdit);

// 添加键盘快捷键支持
document.addEventListener('keydown', (e) => {
    // Ctrl+Enter 或 Cmd+Enter 快速编辑
    if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
        e.preventDefault();
        performImageEdit();
    }
});

// 页面卸载前清理
window.addEventListener('beforeunload', () => {
    // 清理可能的内存引用
    if (window.originalImageData && window.originalImageData.url) {
        URL.revokeObjectURL(window.originalImageData.url);
    }
});

这个核心编辑功能实现了完整的用户体验闭环:

  • 键盘快捷键支持(Ctrl+Enter快速编辑)
  • 图片对比功能,方便用户直观评估编辑效果
  • 下载功能,让用户可以保存结果
  • 详细的处理信息展示
  • 响应式的图片预览和缩放

5. 实用技巧与进阶功能

5.1 提示词优化建议

Qwen-Image-Edit对自然语言指令的理解能力很强,但好的提示词能让效果更上一层楼。我们在前端添加了一个智能提示词助手:

// 提示词模板库
const PROMPT_TEMPLATES = {
    textEdit: [
        "把图片中的文字'{{oldText}}'改为'{{newText}}',保持原有字体和风格",
        "修改标题文字为'{{newText}}',字体加大加粗,颜色改为深蓝色",
        "删除图片中的所有文字,只保留背景和图像内容"
    ],
    background: [
        "把背景替换为{{style}}风格的{{scene}},保持主体人物不变",
        "将背景改为纯色{{color}},添加轻微模糊效果",
        "把背景换成{{location}}场景,保持真实感和光影一致性"
    ],
    style: [
        "将图片转换为{{style}}艺术风格,如梵高、毕加索或水墨画",
        "增强图片的{{effect}}效果,如胶片颗粒、柔焦或HDR",
        "调整图片色调为{{tone}},使整体氛围更{{mood}}"
    ],
    enhancement: [
        "提高图片分辨率至4K,增强细节和清晰度",
        "修复图片中的划痕和噪点,使画面更干净",
        "调整图片亮度和对比度,使主体更突出"
    ]
};

// 提示词生成器
function generatePrompt(templateType, options = {}) {
    const templates = PROMPT_TEMPLATES[templateType] || [];
    if (templates.length === 0) return '';
    
    const template = templates[Math.floor(Math.random() * templates.length)];
    
    // 替换占位符
    let result = template;
    Object.keys(options).forEach(key => {
        const regex = new RegExp(`{{${key}}}`, 'g');
        result = result.replace(regex, options[key]);
    });
    
    return result;
}

// 在页面中添加提示词助手
function addPromptHelper() {
    const helperHtml = `
        <div style="
            margin-top: 15px; padding: 12px; background: #ebf8ff; 
            border-left: 4px solid #4a6fa5; border-radius: 0 4px 4px 0;
        ">
            <h4 style="margin-bottom: 8px; color: #2c5282;"> 提示词小贴士</h4>
            <ul style="margin: 0; padding-left: 20px; color: #4a5568;">
                <li>尽量具体:说"把标题改为'秋季特惠'"比"修改文字"效果更好</li>
                <li>描述细节:加上"字体加大加粗"、"颜色改为深蓝色"等要求</li>
                <li>保持简洁:一句话说明核心需求,避免过多修饰词</li>
                <li>参考示例:<em>"把LOGO换成新设计的蓝色圆形图标,位置移到右上角"</em></li>
            </ul>
        </div>
    `;
    
    const promptGroup = document.querySelector('.input-group:nth-child(1)');
    if (promptGroup) {
        promptGroup.insertAdjacentHTML('afterend', helperHtml);
    }
}

// 添加常用提示词按钮
function addQuickPromptButtons() {
    const promptGroup = document.querySelector('.input-group:nth-child(1)');
    if (!promptGroup)
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐