20 KiB
20 KiB
海报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版本和浏览器支持