711 lines
20 KiB
Markdown
711 lines
20 KiB
Markdown
# 海报Fabric.js前端对接文档
|
||
|
||
## 📋 概述
|
||
|
||
本文档详细说明如何在前端使用新的海报生成API返回的Fabric.js JSON数据,包括图层加载、图片替换、图层管理等功能。
|
||
|
||
## 🔄 核心变化
|
||
|
||
### **从PSD到Fabric.js JSON**
|
||
- **原先**: 返回PSD文件的base64编码
|
||
- **现在**: 返回Fabric.js JSON文件的base64编码 + 原始JSON数据
|
||
- **优势**: 可以直接在前端使用,支持实时编辑和图片替换
|
||
|
||
## 🎯 API接口说明
|
||
|
||
### 1. **生成海报接口**
|
||
|
||
#### **请求示例**
|
||
```javascript
|
||
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();
|
||
```
|
||
|
||
#### **响应结构**
|
||
```javascript
|
||
{
|
||
"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数据**
|
||
```javascript
|
||
// 获取指定海报的JSON数据
|
||
const fabricJson = await fetch(`/poster/fabric-json/${posterId}`)
|
||
.then(res => res.json())
|
||
.then(data => data.data);
|
||
```
|
||
|
||
### 3. **替换底层图片**
|
||
```javascript
|
||
// 单个图片替换
|
||
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. **获取图层信息**
|
||
```javascript
|
||
// 获取图层管理信息
|
||
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结构**
|
||
```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样式**
|
||
```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核心实现**
|
||
|
||
#### **初始化画布**
|
||
```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. **完整使用示例**
|
||
|
||
#### **生成海报并加载到编辑器**
|
||
```javascript
|
||
// 完整的海报生成和加载流程
|
||
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);
|
||
}
|
||
```
|
||
|
||
#### **图片替换功能示例**
|
||
```javascript
|
||
// 高级图片替换功能
|
||
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. 基础集成**
|
||
```html
|
||
<!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. 图片替换示例**
|
||
```javascript
|
||
// 简单的图片替换
|
||
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. 性能优化**
|
||
```javascript
|
||
// 优化Canvas性能
|
||
canvas.set({
|
||
renderOnAddRemove: false, // 添加/删除对象时不自动渲染
|
||
skipTargetFind: true // 跳过目标查找(如果不需要交互)
|
||
});
|
||
|
||
// 批量操作后手动渲染
|
||
canvas.renderAll();
|
||
```
|
||
|
||
### **3. 错误处理**
|
||
```javascript
|
||
// 完善的错误处理
|
||
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. 内存管理**
|
||
```javascript
|
||
// 清理资源
|
||
function cleanup() {
|
||
canvas.dispose(); // 销毁画布
|
||
canvas = null;
|
||
}
|
||
|
||
// 页面卸载时清理
|
||
window.addEventListener('beforeunload', cleanup);
|
||
```
|
||
|
||
## 📞 技术支持
|
||
|
||
如在集成过程中遇到问题:
|
||
- **API问题**: 检查请求格式和响应状态码
|
||
- **Canvas问题**: 查看浏览器控制台错误信息
|
||
- **性能问题**: 检查图片大小和Canvas尺寸
|
||
- **兼容性问题**: 确认Fabric.js版本和浏览器支持
|
||
|
||
## 🔗 相关链接
|
||
|
||
- [Fabric.js官方文档](http://fabricjs.com/docs/)
|
||
- [Canvas API文档](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API)
|
||
- [File API文档](https://developer.mozilla.org/en-US/docs/Web/API/File) |