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

711 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 海报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)