TravelContentCreator/海报Fabric.js前端对接文档.md

20 KiB
Raw Blame History

海报Fabric.js前端对接文档

📋 概述

本文档详细说明如何在前端使用新的海报生成API返回的Fabric.js JSON数据包括图层加载、图片替换、图层管理等功能。

🔄 核心变化

从PSD到Fabric.js JSON

  • 原先: 返回PSD文件的base64编码
  • 现在: 返回Fabric.js JSON文件的base64编码 + 原始JSON数据
  • 优势: 可以直接在前端使用,支持实时编辑和图片替换

🎯 API接口说明

1. 生成海报接口

请求示例

const generatePosterRequest = {
    posterNumber: 1,
    posters: [{
        templateId: "vibrant",
        imagesBase64: "图片ID列表",
        contentId: "内容ID",
        productId: "产品ID",
        generatePsd: true,  // 现在实际生成JSON
        numVariations: 1
    }]
};

const response = await fetch('/poster/generate', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify(generatePosterRequest)
});

const result = await response.json();

响应结构

{
    "code": 0,
    "message": "操作成功",
    "data": [{
        "requestId": "poster-20250104-123456",
        "templateId": "vibrant",
        "resultImagesBase64": [{
            "id": "vibrant_v1",
            "image": "base64编码的PNG图片",
            "format": "PNG",
            "size": [1350, 1800]
        }],
        "psdFiles": [{  // 现在是JSON文件
            "id": "vibrant_v1_json",
            "filename": "template_fabric_v1_20250104.json",
            "data": "base64编码的JSON文件",
            "size": 15672,
            "format": "JSON",
            "jsonData": {  // 原始JSON数据可直接使用
                "version": "5.3.0",
                "objects": [...],
                "background": "white",
                "width": 1350,
                "height": 1800
            }
        }]
    }]
}

2. 获取Fabric.js JSON数据

// 获取指定海报的JSON数据
const fabricJson = await fetch(`/poster/fabric-json/${posterId}`)
    .then(res => res.json())
    .then(data => data.data);

3. 替换底层图片

// 单个图片替换
const replaceImage = async (posterId, newImageBase64) => {
    const formData = new FormData();
    formData.append('posterId', posterId);
    formData.append('newImageBase64', newImageBase64);
    
    const response = await fetch('/poster/replace-image', {
        method: 'POST',
        body: formData
    });
    
    return response.json();
};

4. 获取图层信息

// 获取图层管理信息
const layers = await fetch(`/poster/layers/${posterId}`)
    .then(res => res.json())
    .then(data => data.data);

// 图层信息结构
// [{
//     "name": "图片层",
//     "type": "image",
//     "level": 0,
//     "visible": true,
//     "selectable": true,
//     "replaceable": true
// }]

🛠️ 前端集成实现

1. 基础环境准备

HTML结构

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>海报编辑器</title>
    <script src="https://cdn.jsdelivr.net/npm/fabric@5.3.0/dist/fabric.min.js"></script>
</head>
<body>
    <div id="poster-editor">
        <!-- 画布容器 -->
        <div class="canvas-container">
            <canvas id="poster-canvas" width="1350" height="1800"></canvas>
        </div>
        
        <!-- 图层管理面板 -->
        <div class="layer-panel">
            <h3>图层管理</h3>
            <div id="layer-list"></div>
        </div>
        
        <!-- 图片替换面板 -->
        <div class="image-replace-panel">
            <h3>图片替换</h3>
            <input type="file" id="image-upload" accept="image/*">
            <button id="replace-btn">替换底层图片</button>
        </div>
    </div>
</body>
</html>

CSS样式

#poster-editor {
    display: flex;
    gap: 20px;
    padding: 20px;
}

.canvas-container {
    flex: 1;
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 10px;
    background: #f5f5f5;
}

.layer-panel, .image-replace-panel {
    width: 250px;
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 15px;
    background: white;
}

.layer-item {
    padding: 8px;
    margin: 5px 0;
    border: 1px solid #eee;
    border-radius: 4px;
    cursor: pointer;
}

.layer-item:hover {
    background: #f0f0f0;
}

.layer-item.active {
    background: #e3f2fd;
    border-color: #2196f3;
}

2. JavaScript核心实现

初始化画布

class PosterEditor {
    constructor(canvasId) {
        this.canvas = new fabric.Canvas(canvasId, {
            preserveObjectStacking: true,
            selection: true
        });
        this.currentPosterId = null;
        this.layers = [];
        
        this.initEventListeners();
    }
    
    // 加载海报JSON数据
    async loadPosterFromJson(fabricJsonData, posterId) {
        try {
            console.log('开始加载Fabric.js JSON数据...');
            
            // 清空画布
            this.canvas.clear();
            this.currentPosterId = posterId;
            
            // 加载JSON数据到画布
            await new Promise((resolve, reject) => {
                this.canvas.loadFromJSON(fabricJsonData, () => {
                    console.log('Fabric.js JSON数据加载成功');
                    this.canvas.renderAll();
                    resolve();
                }, (error) => {
                    console.error('加载JSON数据失败:', error);
                    reject(error);
                });
            });
            
            // 加载图层信息
            await this.loadLayerInfo(posterId);
            
            console.log('海报加载完成');
            
        } catch (error) {
            console.error('加载海报失败:', error);
            throw error;
        }
    }
    
    // 从API获取并加载海报
    async loadPosterFromApi(posterId) {
        try {
            const response = await fetch(`/poster/fabric-json/${posterId}`);
            const result = await response.json();
            
            if (result.code === 0) {
                await this.loadPosterFromJson(result.data, posterId);
            } else {
                throw new Error(result.message || '获取海报数据失败');
            }
        } catch (error) {
            console.error('从API加载海报失败:', error);
            throw error;
        }
    }
    
    // 加载图层信息
    async loadLayerInfo(posterId) {
        try {
            const response = await fetch(`/poster/layers/${posterId}`);
            const result = await response.json();
            
            if (result.code === 0) {
                this.layers = result.data;
                this.renderLayerPanel();
            }
        } catch (error) {
            console.error('加载图层信息失败:', error);
        }
    }
    
    // 渲染图层管理面板
    renderLayerPanel() {
        const layerList = document.getElementById('layer-list');
        layerList.innerHTML = '';
        
        this.layers.forEach((layer, index) => {
            const layerItem = document.createElement('div');
            layerItem.className = 'layer-item';
            layerItem.innerHTML = `
                <div class="layer-info">
                    <span class="layer-name">${layer.name}</span>
                    <span class="layer-type">(${layer.type})</span>
                </div>
                <div class="layer-controls">
                    <label>
                        <input type="checkbox" ${layer.visible ? 'checked' : ''} 
                               onchange="editor.toggleLayerVisibility(${index})">
                        显示
                    </label>
                    ${layer.replaceable ? `
                        <button onclick="editor.selectLayerForReplace(${index})">
                            替换图片
                        </button>
                    ` : ''}
                </div>
            `;
            
            layerList.appendChild(layerItem);
        });
    }
    
    // 替换底层图片
    async replaceBackgroundImage(imageFile) {
        try {
            if (!this.currentPosterId) {
                throw new Error('没有加载的海报');
            }
            
            // 转换图片为base64
            const imageBase64 = await this.fileToBase64(imageFile);
            
            // 调用后端API替换图片
            const formData = new FormData();
            formData.append('posterId', this.currentPosterId);
            formData.append('newImageBase64', imageBase64);
            
            const response = await fetch('/poster/replace-image', {
                method: 'POST',
                body: formData
            });
            
            const result = await response.json();
            
            if (result.code === 0) {
                // 重新加载画布
                await this.loadPosterFromJson(result.data, this.currentPosterId);
                console.log('图片替换成功');
            } else {
                throw new Error(result.message || '图片替换失败');
            }
            
        } catch (error) {
            console.error('替换图片失败:', error);
            alert('替换图片失败: ' + error.message);
        }
    }
    
    // 文件转base64
    fileToBase64(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => {
                // 移除data:image/xxx;base64,前缀
                const base64 = reader.result.split(',')[1];
                resolve(base64);
            };
            reader.onerror = reject;
            reader.readAsDataURL(file);
        });
    }
    
    // 切换图层可见性
    toggleLayerVisibility(layerIndex) {
        const layer = this.layers[layerIndex];
        if (layer) {
            layer.visible = !layer.visible;
            // 这里可以实现图层显示/隐藏的逻辑
            // 需要根据具体的图层结构来实现
        }
    }
    
    // 初始化事件监听
    initEventListeners() {
        // 图片上传事件
        document.getElementById('image-upload').addEventListener('change', (e) => {
            const file = e.target.files[0];
            if (file) {
                this.replaceBackgroundImage(file);
            }
        });
        
        // 替换按钮事件
        document.getElementById('replace-btn').addEventListener('click', () => {
            document.getElementById('image-upload').click();
        });
    }
    
    // 导出当前画布为图片
    exportAsImage(format = 'png', quality = 1.0) {
        return this.canvas.toDataURL({
            format: format,
            quality: quality,
            multiplier: 1
        });
    }
    
    // 获取当前的Fabric.js JSON数据
    getCurrentJson() {
        return this.canvas.toJSON();
    }
}

// 初始化编辑器
const editor = new PosterEditor('poster-canvas');

3. 完整使用示例

生成海报并加载到编辑器

// 完整的海报生成和加载流程
async function generateAndLoadPoster() {
    try {
        // 1. 生成海报
        const generateRequest = {
            posterNumber: 1,
            posters: [{
                templateId: "vibrant",
                imagesBase64: "123,456", // 图片ID列表
                generatePsd: true,
                numVariations: 1
            }]
        };
        
        console.log('正在生成海报...');
        const generateResponse = await fetch('/poster/generate', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify(generateRequest)
        });
        
        const generateResult = await generateResponse.json();
        
        if (generateResult.code !== 0) {
            throw new Error('海报生成失败: ' + generateResult.message);
        }
        
        const posterData = generateResult.data[0];
        const fabricJsonData = posterData.psdFiles[0].jsonData;
        const posterId = posterData.psdFiles[0].id;
        
        console.log('海报生成成功,开始加载到编辑器...');
        
        // 2. 加载到编辑器
        await editor.loadPosterFromJson(fabricJsonData, posterId);
        
        console.log('海报加载到编辑器成功!');
        
        // 3. 显示预览图
        const previewImage = posterData.resultImagesBase64[0].image;
        showPreviewImage(previewImage);
        
    } catch (error) {
        console.error('生成和加载海报失败:', error);
        alert('操作失败: ' + error.message);
    }
}

// 显示预览图
function showPreviewImage(base64Image) {
    const previewDiv = document.createElement('div');
    previewDiv.innerHTML = `
        <h3>生成的海报预览</h3>
        <img src="data:image/png;base64,${base64Image}" 
             style="max-width: 300px; border: 1px solid #ddd; border-radius: 8px;">
    `;
    document.body.appendChild(previewDiv);
}

图片替换功能示例

// 高级图片替换功能
class ImageReplacer {
    constructor(editor) {
        this.editor = editor;
        this.setupUI();
    }
    
    setupUI() {
        const replacePanel = document.querySelector('.image-replace-panel');
        replacePanel.innerHTML = `
            <h3>图片替换</h3>
            <div class="upload-area" id="upload-area">
                <p>拖拽图片到这里或点击选择</p>
                <input type="file" id="image-upload" accept="image/*" style="display: none;">
            </div>
            <div class="image-preview" id="image-preview" style="display: none;">
                <img id="preview-img" style="max-width: 100%; border-radius: 4px;">
                <button id="confirm-replace">确认替换</button>
                <button id="cancel-replace">取消</button>
            </div>
        `;
        
        this.bindEvents();
    }
    
    bindEvents() {
        const uploadArea = document.getElementById('upload-area');
        const fileInput = document.getElementById('image-upload');
        const previewDiv = document.getElementById('image-preview');
        const previewImg = document.getElementById('preview-img');
        
        // 点击上传
        uploadArea.addEventListener('click', () => {
            fileInput.click();
        });
        
        // 拖拽上传
        uploadArea.addEventListener('dragover', (e) => {
            e.preventDefault();
            uploadArea.style.backgroundColor = '#f0f0f0';
        });
        
        uploadArea.addEventListener('dragleave', () => {
            uploadArea.style.backgroundColor = '';
        });
        
        uploadArea.addEventListener('drop', (e) => {
            e.preventDefault();
            uploadArea.style.backgroundColor = '';
            
            const files = e.dataTransfer.files;
            if (files.length > 0) {
                this.handleFileSelect(files[0]);
            }
        });
        
        // 文件选择
        fileInput.addEventListener('change', (e) => {
            const file = e.target.files[0];
            if (file) {
                this.handleFileSelect(file);
            }
        });
        
        // 确认替换
        document.getElementById('confirm-replace').addEventListener('click', () => {
            this.confirmReplace();
        });
        
        // 取消
        document.getElementById('cancel-replace').addEventListener('click', () => {
            this.cancelReplace();
        });
    }
    
    handleFileSelect(file) {
        const reader = new FileReader();
        reader.onload = (e) => {
            const previewImg = document.getElementById('preview-img');
            const previewDiv = document.getElementById('image-preview');
            const uploadArea = document.getElementById('upload-area');
            
            previewImg.src = e.target.result;
            previewDiv.style.display = 'block';
            uploadArea.style.display = 'none';
            
            this.selectedFile = file;
        };
        reader.readAsDataURL(file);
    }
    
    async confirmReplace() {
        try {
            await this.editor.replaceBackgroundImage(this.selectedFile);
            this.cancelReplace();
            alert('图片替换成功!');
        } catch (error) {
            alert('图片替换失败: ' + error.message);
        }
    }
    
    cancelReplace() {
        document.getElementById('image-preview').style.display = 'none';
        document.getElementById('upload-area').style.display = 'block';
        this.selectedFile = null;
    }
}

// 初始化图片替换器
const imageReplacer = new ImageReplacer(editor);

🚀 快速开始

1. 基础集成

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/fabric@5.3.0/dist/fabric.min.js"></script>
</head>
<body>
    <canvas id="canvas" width="1350" height="1800"></canvas>
    <button onclick="loadDemo()">加载示例海报</button>
    
    <script>
        const canvas = new fabric.Canvas('canvas');
        
        async function loadDemo() {
            // 从API获取海报数据
            const response = await fetch('/poster/fabric-json/demo-poster-id');
            const result = await response.json();
            
            // 加载到画布
            canvas.loadFromJSON(result.data, () => {
                canvas.renderAll();
                console.log('海报加载完成!');
            });
        }
    </script>
</body>
</html>

2. 图片替换示例

// 简单的图片替换
async function replaceImage() {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*';
    
    input.onchange = async (e) => {
        const file = e.target.files[0];
        if (!file) return;
        
        const reader = new FileReader();
        reader.onload = async (event) => {
            const base64 = event.target.result.split(',')[1];
            
            try {
                const response = await fetch('/poster/replace-image', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                    body: `posterId=your-poster-id&newImageBase64=${encodeURIComponent(base64)}`
                });
                
                const result = await response.json();
                if (result.code === 0) {
                    // 重新加载画布
                    canvas.loadFromJSON(result.data, () => {
                        canvas.renderAll();
                    });
                }
            } catch (error) {
                console.error('替换失败:', error);
            }
        };
        reader.readAsDataURL(file);
    };
    
    input.click();
}

⚠️ 注意事项

1. 浏览器兼容性

  • 需要支持Canvas API
  • 需要支持File API
  • 建议使用现代浏览器Chrome 80+, Firefox 75+, Safari 13+

2. 性能优化

// 优化Canvas性能
canvas.set({
    renderOnAddRemove: false,  // 添加/删除对象时不自动渲染
    skipTargetFind: true       // 跳过目标查找(如果不需要交互)
});

// 批量操作后手动渲染
canvas.renderAll();

3. 错误处理

// 完善的错误处理
try {
    await editor.loadPosterFromApi(posterId);
} catch (error) {
    if (error.message.includes('404')) {
        console.error('海报不存在');
    } else if (error.message.includes('网络')) {
        console.error('网络错误,请重试');
    } else {
        console.error('未知错误:', error);
    }
}

4. 内存管理

// 清理资源
function cleanup() {
    canvas.dispose();  // 销毁画布
    canvas = null;
}

// 页面卸载时清理
window.addEventListener('beforeunload', cleanup);

📞 技术支持

如在集成过程中遇到问题:

  • API问题: 检查请求格式和响应状态码
  • Canvas问题: 查看浏览器控制台错误信息
  • 性能问题: 检查图片大小和Canvas尺寸
  • 兼容性问题: 确认Fabric.js版本和浏览器支持

🔗 相关链接