修改了接受的格式
This commit is contained in:
parent
9cae3ec061
commit
dc639e8d39
Binary file not shown.
Binary file not shown.
@ -14,7 +14,7 @@ from pydantic import BaseModel, Field
|
||||
class PosterGenerateRequest(BaseModel):
|
||||
"""海报生成请求模型"""
|
||||
templateId: str = Field("vibrant", description="模板ID")
|
||||
imagesBase64: Optional[str] = Field(None, description="图像base64编码")
|
||||
imagesBase64: Optional[List[str]] = Field(None, description="图像base64编码列表")
|
||||
posterContent: Optional[Dict[str, Any]] = Field(None, description="海报内容,如果提供则直接使用此内容")
|
||||
contentId: Optional[str] = Field(None, description="内容ID,用于AI生成内容")
|
||||
productId: Optional[str] = Field(None, description="产品ID,用于AI生成内容")
|
||||
@ -28,7 +28,7 @@ class PosterGenerateRequest(BaseModel):
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"templateId": "vibrant",
|
||||
"imagesBase64": "",
|
||||
"imagesBase64": ["base64_encoded_image_1", "base64_encoded_image_2"],
|
||||
"numVariations": 1,
|
||||
"forceLlmGeneration":False,
|
||||
"generatePsd": True,
|
||||
|
||||
@ -69,7 +69,7 @@ class ContentRequest(BaseModel):
|
||||
audienceIds: Optional[List[Union[int, str]]] = Field(None, description="受众ID列表")
|
||||
scenicSpotIds: Optional[List[Union[int, str]]] = Field(None, description="景区ID列表")
|
||||
productIds: Optional[List[Union[int, str]]] = Field(None, description="产品ID列表")
|
||||
autoJudge: bool = Field(False, description="是否自动进行内容审核")
|
||||
autoJudge: Optional[bool] = Field(True, description="是否自动进行内容审核")
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
@ -185,7 +185,7 @@ class PipelineRequest(BaseModel):
|
||||
scenicSpotIds: Optional[List[Union[int, str]]] = Field(None, description="景区ID列表")
|
||||
productIds: Optional[List[Union[int, str]]] = Field(None, description="产品ID列表")
|
||||
skipJudge: bool = Field(False, description="是否跳过内容审核步骤")
|
||||
autoJudge: bool = Field(False, description="是否在内容生成时进行内嵌审核")
|
||||
autoJudge: Optional[bool] = Field(None, description="是否在内容生成时进行内嵌审核")
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -234,26 +234,37 @@ class PosterService:
|
||||
# 获取模板的默认尺寸,如果获取不到则使用标准尺寸
|
||||
template_size = getattr(template_handler, 'size', (900, 1200))
|
||||
|
||||
if images_base64 and images_base64.strip():
|
||||
if images_base64 and len(images_base64) > 0:
|
||||
try:
|
||||
logger.info(f"🎯 接收到 {len(images_base64)} 张图片的base64数据")
|
||||
|
||||
# 处理第一张图片(目前模板只支持单张图片)
|
||||
# 未来可以扩展为处理多张图片
|
||||
first_image_base64 = images_base64[0] if len(images_base64) > 0 else ""
|
||||
|
||||
if not first_image_base64 or not first_image_base64.strip():
|
||||
raise ValueError("第一张图片的base64数据为空")
|
||||
|
||||
logger.info(f"📊 处理第一张图片,base64长度: {len(first_image_base64)}")
|
||||
|
||||
# 移除可能存在的MIME类型前缀
|
||||
if images_base64.startswith("data:"):
|
||||
images_base64 = images_base64.split(",", 1)[1]
|
||||
if first_image_base64.startswith("data:"):
|
||||
first_image_base64 = first_image_base64.split(",", 1)[1]
|
||||
|
||||
# 彻底清理base64字符串 - 移除所有空白字符
|
||||
images_base64 = ''.join(images_base64.split())
|
||||
first_image_base64 = ''.join(first_image_base64.split())
|
||||
|
||||
# 验证base64字符串长度(应该是4的倍数)
|
||||
if len(images_base64) % 4 != 0:
|
||||
if len(first_image_base64) % 4 != 0:
|
||||
# 添加必要的填充
|
||||
images_base64 += '=' * (4 - len(images_base64) % 4)
|
||||
logger.info(f"为base64字符串添加了填充,最终长度: {len(images_base64)}")
|
||||
first_image_base64 += '=' * (4 - len(first_image_base64) % 4)
|
||||
logger.info(f"为base64字符串添加了填充,最终长度: {len(first_image_base64)}")
|
||||
|
||||
logger.info(f"准备解码base64数据,长度: {len(images_base64)}, 前20字符: {images_base64[:20]}...")
|
||||
logger.info(f"准备解码base64数据,长度: {len(first_image_base64)}, 前20字符: {first_image_base64[:20]}...")
|
||||
|
||||
# 解码base64
|
||||
image_bytes = base64.b64decode(images_base64)
|
||||
logger.info(f"base64解码成功,图片数据大小: {len(image_bytes)} bytes")
|
||||
image_bytes = base64.b64decode(first_image_base64)
|
||||
logger.info(f"✅ base64解码成功,图片数据大小: {len(image_bytes)} bytes")
|
||||
|
||||
# 验证解码后的数据不为空
|
||||
if len(image_bytes) == 0:
|
||||
@ -262,11 +273,11 @@ class PosterService:
|
||||
# 检查文件头判断图片格式
|
||||
file_header = image_bytes[:10]
|
||||
if file_header.startswith(b'\xff\xd8\xff'):
|
||||
logger.info("检测到JPEG格式图片")
|
||||
logger.info("✅ 检测到JPEG格式图片")
|
||||
elif file_header.startswith(b'\x89PNG'):
|
||||
logger.info("检测到PNG格式图片")
|
||||
logger.info("✅ 检测到PNG格式图片")
|
||||
else:
|
||||
logger.warning(f"未识别的图片格式,文件头: {file_header.hex()}")
|
||||
logger.warning(f"⚠️ 未识别的图片格式,文件头: {file_header.hex()}")
|
||||
|
||||
# 创建PIL Image对象
|
||||
image_io = BytesIO(image_bytes)
|
||||
@ -279,16 +290,16 @@ class PosterService:
|
||||
image_io.seek(0)
|
||||
images = Image.open(image_io)
|
||||
|
||||
logger.info(f"图片解码成功,格式: {images.format}, 尺寸: {images.size}, 模式: {images.mode}")
|
||||
logger.info(f"✅ 图片解码成功,格式: {images.format}, 尺寸: {images.size}, 模式: {images.mode}")
|
||||
|
||||
except binascii.Error as e:
|
||||
logger.error(f"Base64解码失败: {e}")
|
||||
logger.error(f"问题数据长度: {len(images_base64) if 'images_base64' in locals() else 'unknown'}")
|
||||
logger.error(f"❌ Base64解码失败: {e}")
|
||||
logger.error(f"问题数据长度: {len(first_image_base64) if 'first_image_base64' in locals() else 'unknown'}")
|
||||
# 创建一个与目标大小一致的透明底图
|
||||
images = Image.new('RGBA', template_size, color=(0, 0, 0, 0))
|
||||
logger.info(f"创建默认透明背景图,尺寸: {template_size}")
|
||||
except Exception as e:
|
||||
logger.error(f"图片处理失败: {e}")
|
||||
logger.error(f"❌ 图片处理失败: {e}")
|
||||
logger.error(f"错误类型: {type(e).__name__}")
|
||||
if 'image_bytes' in locals():
|
||||
logger.error(f"图片数据大小: {len(image_bytes)} bytes, 前20字节: {image_bytes[:20].hex()}")
|
||||
@ -296,7 +307,7 @@ class PosterService:
|
||||
images = Image.new('RGBA', template_size, color=(0, 0, 0, 0))
|
||||
logger.info(f"创建默认透明背景图,尺寸: {template_size}")
|
||||
else:
|
||||
logger.warning("未提供图片数据,使用默认透明背景图")
|
||||
logger.warning("⚠️ 未提供图片数据,使用默认透明背景图")
|
||||
# 创建一个与目标大小一致的透明底图
|
||||
images = Image.new('RGBA', template_size, color=(0, 0, 0, 0))
|
||||
logger.info(f"创建默认透明背景图,尺寸: {template_size}")
|
||||
|
||||
@ -10,6 +10,8 @@ import logging
|
||||
import uuid
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from datetime import datetime
|
||||
import re
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
from core.config import ConfigManager, GenerateTopicConfig, GenerateContentConfig
|
||||
from core.ai import AIAgent
|
||||
@ -24,6 +26,249 @@ from api.services.database_service import DatabaseService
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TopicIDMappingManager:
|
||||
"""选题ID映射管理器 - 专门处理选题中的对象ID映射"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化映射管理器"""
|
||||
self.name_to_id = {} # 名称 -> ID映射
|
||||
self.mapping_data = {
|
||||
'styles': {},
|
||||
'audiences': {},
|
||||
'scenic_spots': {},
|
||||
'products': {}
|
||||
}
|
||||
|
||||
def add_objects_mapping(self,
|
||||
style_objects: Optional[List[Dict[str, Any]]] = None,
|
||||
audience_objects: Optional[List[Dict[str, Any]]] = None,
|
||||
scenic_spot_objects: Optional[List[Dict[str, Any]]] = None,
|
||||
product_objects: Optional[List[Dict[str, Any]]] = None):
|
||||
"""
|
||||
批量添加对象映射关系
|
||||
|
||||
Args:
|
||||
style_objects: 风格对象列表
|
||||
audience_objects: 受众对象列表
|
||||
scenic_spot_objects: 景区对象列表
|
||||
product_objects: 产品对象列表
|
||||
"""
|
||||
logger.info("开始建立选题ID映射关系")
|
||||
mapping_count = 0
|
||||
|
||||
if style_objects:
|
||||
logger.info(f"处理 {len(style_objects)} 个风格对象")
|
||||
for obj in style_objects:
|
||||
name = obj.get('styleName', '')
|
||||
obj_id = obj.get('id')
|
||||
if name and obj_id:
|
||||
self.mapping_data['styles'][name] = str(obj_id)
|
||||
self._add_name_variants('styles', name, str(obj_id))
|
||||
mapping_count += 1
|
||||
logger.info(f"添加风格映射: {name} -> ID {obj_id}")
|
||||
|
||||
if audience_objects:
|
||||
logger.info(f"处理 {len(audience_objects)} 个受众对象")
|
||||
for obj in audience_objects:
|
||||
name = obj.get('audienceName', '')
|
||||
obj_id = obj.get('id')
|
||||
if name and obj_id:
|
||||
self.mapping_data['audiences'][name] = str(obj_id)
|
||||
self._add_name_variants('audiences', name, str(obj_id))
|
||||
mapping_count += 1
|
||||
logger.info(f"添加受众映射: {name} -> ID {obj_id}")
|
||||
|
||||
if scenic_spot_objects:
|
||||
logger.info(f"处理 {len(scenic_spot_objects)} 个景区对象")
|
||||
for obj in scenic_spot_objects:
|
||||
name = obj.get('name', '')
|
||||
obj_id = obj.get('id')
|
||||
if name and obj_id:
|
||||
self.mapping_data['scenic_spots'][name] = str(obj_id)
|
||||
self._add_name_variants('scenic_spots', name, str(obj_id))
|
||||
mapping_count += 1
|
||||
logger.info(f"添加景区映射: {name} -> ID {obj_id}")
|
||||
|
||||
if product_objects:
|
||||
logger.info(f"处理 {len(product_objects)} 个产品对象")
|
||||
for obj in product_objects:
|
||||
name = obj.get('productName', '')
|
||||
obj_id = obj.get('id')
|
||||
if name and obj_id:
|
||||
self.mapping_data['products'][name] = str(obj_id)
|
||||
self._add_name_variants('products', name, str(obj_id))
|
||||
mapping_count += 1
|
||||
logger.info(f"添加产品映射: {name} -> ID {obj_id}")
|
||||
|
||||
total_variants = len(self.name_to_id)
|
||||
logger.info(f"ID映射关系建立完成: {mapping_count} 个对象,生成 {total_variants} 个名称变体")
|
||||
|
||||
def _add_name_variants(self, category: str, name: str, obj_id: str):
|
||||
"""为名称添加各种变体以支持模糊匹配"""
|
||||
variants = [name]
|
||||
|
||||
# 去除标点符号版本
|
||||
clean_name = re.sub(r'[^\w\s]', '', name)
|
||||
if clean_name != name:
|
||||
variants.append(clean_name)
|
||||
|
||||
# 去除空格版本
|
||||
no_space_name = name.replace(' ', '')
|
||||
if no_space_name != name:
|
||||
variants.append(no_space_name)
|
||||
|
||||
# 添加简化版本(去除常见后缀)
|
||||
simplified = re.sub(r'(风格|类型|系列|产品|景区|公园)$', '', name)
|
||||
if simplified != name and simplified:
|
||||
variants.append(simplified)
|
||||
|
||||
# 为所有变体添加映射
|
||||
for variant in variants:
|
||||
self.name_to_id[variant.lower()] = {
|
||||
'category': category,
|
||||
'id': obj_id,
|
||||
'original_name': name
|
||||
}
|
||||
|
||||
# 记录生成的变体(仅在有多个变体时记录)
|
||||
if len(variants) > 1:
|
||||
variants_str = ', '.join(f"'{v}'" for v in variants)
|
||||
logger.info(f" 为 '{name}' 生成 {len(variants)} 个变体: {variants_str}")
|
||||
|
||||
def find_ids_in_topic(self, topic: Dict[str, Any]) -> Dict[str, List[str]]:
|
||||
"""
|
||||
在选题中查找相关的对象ID
|
||||
|
||||
Args:
|
||||
topic: 选题字典
|
||||
|
||||
Returns:
|
||||
按类型分组的ID列表
|
||||
"""
|
||||
topic_index = topic.get('index', 'N/A')
|
||||
|
||||
found_ids = {
|
||||
'style_ids': [],
|
||||
'audience_ids': [],
|
||||
'scenic_spot_ids': [],
|
||||
'product_ids': []
|
||||
}
|
||||
|
||||
# 提取选题中的所有文本
|
||||
topic_text = self._extract_topic_text(topic)
|
||||
topic_text_lower = topic_text.lower()
|
||||
|
||||
logger.info(f"开始为选题 {topic_index} 进行ID匹配")
|
||||
logger.info(f"选题 {topic_index} 提取的文本内容: '{topic_text}'")
|
||||
logger.info(f"当前映射库包含 {len(self.name_to_id)} 个名称变体")
|
||||
|
||||
# 显示映射库内容
|
||||
logger.info("映射库内容:")
|
||||
for variant, info in self.name_to_id.items():
|
||||
logger.info(f" '{variant}' -> {info['original_name']} ({info['category']}, ID: {info['id']})")
|
||||
|
||||
# 记录匹配过程
|
||||
match_details = []
|
||||
|
||||
# 在文本中查找匹配的名称
|
||||
for name_variant, mapping_info in self.name_to_id.items():
|
||||
if name_variant in topic_text_lower:
|
||||
category = mapping_info['category']
|
||||
obj_id = mapping_info['id']
|
||||
original_name = mapping_info['original_name']
|
||||
|
||||
# 记录匹配详情
|
||||
match_details.append({
|
||||
'variant': name_variant,
|
||||
'original_name': original_name,
|
||||
'category': category,
|
||||
'id': obj_id
|
||||
})
|
||||
|
||||
# 映射到返回格式
|
||||
if category == 'styles' and obj_id not in found_ids['style_ids']:
|
||||
found_ids['style_ids'].append(obj_id)
|
||||
logger.info(f"选题 {topic_index} 匹配到风格: '{name_variant}' -> {original_name} (ID: {obj_id})")
|
||||
elif category == 'audiences' and obj_id not in found_ids['audience_ids']:
|
||||
found_ids['audience_ids'].append(obj_id)
|
||||
logger.info(f"选题 {topic_index} 匹配到受众: '{name_variant}' -> {original_name} (ID: {obj_id})")
|
||||
elif category == 'scenic_spots' and obj_id not in found_ids['scenic_spot_ids']:
|
||||
found_ids['scenic_spot_ids'].append(obj_id)
|
||||
logger.info(f"选题 {topic_index} 匹配到景区: '{name_variant}' -> {original_name} (ID: {obj_id})")
|
||||
elif category == 'products' and obj_id not in found_ids['product_ids']:
|
||||
found_ids['product_ids'].append(obj_id)
|
||||
logger.info(f"选题 {topic_index} 匹配到产品: '{name_variant}' -> {original_name} (ID: {obj_id})")
|
||||
|
||||
# 统计匹配结果
|
||||
total_found = sum(len(ids) for ids in found_ids.values())
|
||||
total_available = len(set(info['category'] + '_' + info['id'] for info in self.name_to_id.values()))
|
||||
match_rate = (total_found / total_available * 100) if total_available > 0 else 0
|
||||
|
||||
# 输出详细的匹配结果
|
||||
if total_found > 0:
|
||||
logger.info(f"选题 {topic_index} ID匹配完成: 找到 {total_found} 个相关对象,匹配率: {match_rate:.1f}%")
|
||||
logger.info(f"选题 {topic_index} 匹配详情: {match_details}")
|
||||
|
||||
# 按类别显示匹配结果
|
||||
for category, ids in found_ids.items():
|
||||
if ids:
|
||||
category_name = {
|
||||
'style_ids': '风格',
|
||||
'audience_ids': '受众',
|
||||
'scenic_spot_ids': '景区',
|
||||
'product_ids': '产品'
|
||||
}.get(category, category)
|
||||
|
||||
# 获取匹配的原始名称
|
||||
matched_names = []
|
||||
# 建立正确的分类映射关系
|
||||
category_mapping = {
|
||||
'style_ids': 'styles',
|
||||
'audience_ids': 'audiences',
|
||||
'scenic_spot_ids': 'scenic_spots',
|
||||
'product_ids': 'products'
|
||||
}
|
||||
target_category = category_mapping.get(category)
|
||||
|
||||
logger.info(f"选题 {topic_index} 处理分类 {category} -> {target_category}, IDs: {ids}")
|
||||
|
||||
for detail in match_details:
|
||||
if detail['id'] in ids and detail['category'] == target_category:
|
||||
matched_names.append(f"{detail['original_name']}({detail['id']})")
|
||||
|
||||
# 显示分类结果
|
||||
names_str = ', '.join(matched_names) if matched_names else '(无匹配名称)'
|
||||
logger.info(f" {category_name}: {names_str}")
|
||||
|
||||
# 如果没有匹配名称,输出调试信息
|
||||
if not matched_names:
|
||||
logger.info(f"选题 {topic_index} {category_name}无匹配名称,详情检查:")
|
||||
logger.info(f" IDs: {ids}")
|
||||
logger.info(f" 目标分类: {target_category}")
|
||||
for detail in match_details:
|
||||
if detail['id'] in ids:
|
||||
logger.info(f" 详情: {detail}")
|
||||
else:
|
||||
logger.info(f"选题 {topic_index} 未匹配到任何相关对象ID")
|
||||
|
||||
return found_ids
|
||||
|
||||
def _extract_topic_text(self, topic: Dict[str, Any]) -> str:
|
||||
"""提取选题中的所有文本内容"""
|
||||
text_parts = []
|
||||
|
||||
# 提取主要字段的文本
|
||||
text_fields = ['object', 'style', 'targetAudience', 'product', 'logic',
|
||||
'productLogic', 'styleLogic', 'targetAudienceLogic']
|
||||
|
||||
for field in text_fields:
|
||||
if field in topic and topic[field]:
|
||||
text_parts.append(str(topic[field]))
|
||||
|
||||
return ' '.join(text_parts)
|
||||
|
||||
|
||||
|
||||
class TweetService:
|
||||
"""文字内容服务类"""
|
||||
|
||||
@ -49,6 +294,9 @@ class TweetService:
|
||||
self.prompt_service = PromptService(config_manager)
|
||||
self.prompt_builder = PromptBuilderService(config_manager, self.prompt_service)
|
||||
|
||||
# 初始化选题ID映射管理器
|
||||
self.topic_id_mapping_manager = TopicIDMappingManager()
|
||||
|
||||
async def generate_topics(self, dates: Optional[str] = None, numTopics: int = 5,
|
||||
styles: Optional[List[str]] = None,
|
||||
audiences: Optional[List[str]] = None,
|
||||
@ -84,6 +332,14 @@ class TweetService:
|
||||
topic_config.topic.date = dates
|
||||
topic_config.topic.num = numTopics
|
||||
|
||||
# 建立ID映射关系(用于后续的ID反向映射)
|
||||
self.topic_id_mapping_manager.add_objects_mapping(
|
||||
style_objects=style_objects,
|
||||
audience_objects=audience_objects,
|
||||
scenic_spot_objects=scenic_spot_objects,
|
||||
product_objects=product_objects
|
||||
)
|
||||
|
||||
# 使用PromptBuilderService构建提示词
|
||||
system_prompt, user_prompt = self.prompt_builder.build_topic_prompt(
|
||||
products=products,
|
||||
@ -103,12 +359,35 @@ class TweetService:
|
||||
if not topics:
|
||||
logger.error("未能生成任何选题")
|
||||
return str(uuid.uuid4()), []
|
||||
|
||||
# 为每个选题添加相关的对象ID
|
||||
logger.info(f"开始为 {len(topics)} 个选题进行ID映射分析")
|
||||
enhanced_topics = []
|
||||
total_mapped_topics = 0
|
||||
|
||||
for topic in topics:
|
||||
enhanced_topic = topic.copy()
|
||||
|
||||
# 查找选题中涉及的对象ID
|
||||
related_object_ids = self.topic_id_mapping_manager.find_ids_in_topic(topic)
|
||||
|
||||
# 只有当找到相关ID时才添加字段
|
||||
if any(related_object_ids.values()):
|
||||
enhanced_topic['related_object_ids'] = related_object_ids
|
||||
total_mapped_topics += 1
|
||||
logger.info(f"选题 {topic.get('index', 'N/A')} 找到相关ID: {related_object_ids}")
|
||||
|
||||
enhanced_topics.append(enhanced_topic)
|
||||
|
||||
# 统计总体映射情况
|
||||
mapping_coverage = (total_mapped_topics / len(topics) * 100) if topics else 0
|
||||
logger.info(f"选题ID映射完成: {total_mapped_topics}/{len(topics)} 个选题包含相关对象ID,覆盖率: {mapping_coverage:.1f}%")
|
||||
|
||||
# 生成请求ID
|
||||
requestId = f"topic-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}"
|
||||
|
||||
logger.info(f"选题生成完成,请求ID: {requestId}, 数量: {len(topics)}")
|
||||
return requestId, topics
|
||||
logger.info(f"选题生成完成,请求ID: {requestId}, 数量: {len(enhanced_topics)}")
|
||||
return requestId, enhanced_topics
|
||||
|
||||
async def generate_content(self, topic: Optional[Dict[str, Any]] = None, autoJudge: bool = False,
|
||||
style_objects: Optional[List[Dict[str, Any]]] = None,
|
||||
@ -198,10 +477,10 @@ class TweetService:
|
||||
|
||||
async def _enhance_topic_with_database_data(self, topic: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
使用数据库数据增强选题信息
|
||||
使用数据库数据增强选题信息,优先使用related_object_ids中的反射ID
|
||||
|
||||
Args:
|
||||
topic: 原始选题数据
|
||||
topic: 原始选题数据(可能包含related_object_ids字段)
|
||||
|
||||
Returns:
|
||||
增强后的选题数据
|
||||
@ -216,46 +495,119 @@ class TweetService:
|
||||
logger.warning("数据库服务不可用,无法增强选题数据")
|
||||
return enhanced_topic
|
||||
|
||||
# 优先使用related_object_ids中的反射ID(更精确)
|
||||
related_ids = topic.get('related_object_ids', {})
|
||||
if related_ids:
|
||||
logger.info(f"选题包含反射ID信息,优先使用: {related_ids}")
|
||||
enhanced_topic = await self._enhance_with_related_ids(enhanced_topic, db_service, related_ids)
|
||||
return enhanced_topic
|
||||
|
||||
# 处理风格ID
|
||||
if 'styleIds' in topic and topic['styleIds']:
|
||||
style_id = topic['styleIds'][0] if isinstance(topic['styleIds'], list) else topic['styleIds']
|
||||
style_data = db_service.get_style_by_id(style_id)
|
||||
if style_data:
|
||||
style_name = style_data.get('styleName')
|
||||
enhanced_topic['style_object'] = style_data
|
||||
enhanced_topic['style'] = style_data.get('styleName')
|
||||
logger.info(f"从数据库加载风格数据: {style_data.get('styleName')} (ID: {style_id})")
|
||||
enhanced_topic['style'] = style_name
|
||||
logger.info(f"从数据库加载风格数据: {style_name} (ID: {style_id})")
|
||||
|
||||
# 处理受众ID
|
||||
if 'audienceIds' in topic and topic['audienceIds']:
|
||||
audience_id = topic['audienceIds'][0] if isinstance(topic['audienceIds'], list) else topic['audienceIds']
|
||||
audience_data = db_service.get_audience_by_id(audience_id)
|
||||
if audience_data:
|
||||
audience_name = audience_data.get('audienceName')
|
||||
enhanced_topic['audience_object'] = audience_data
|
||||
enhanced_topic['targetAudience'] = audience_data.get('audienceName')
|
||||
logger.info(f"从数据库加载受众数据: {audience_data.get('audienceName')} (ID: {audience_id})")
|
||||
enhanced_topic['targetAudience'] = audience_name
|
||||
logger.info(f"从数据库加载受众数据: {audience_name} (ID: {audience_id})")
|
||||
|
||||
# 处理景区ID
|
||||
if 'scenicSpotIds' in topic and topic['scenicSpotIds']:
|
||||
spot_id = topic['scenicSpotIds'][0] if isinstance(topic['scenicSpotIds'], list) else topic['scenicSpotIds']
|
||||
spot_data = db_service.get_scenic_spot_by_id(spot_id)
|
||||
if spot_data:
|
||||
spot_name = spot_data.get('name')
|
||||
enhanced_topic['scenic_spot_object'] = spot_data
|
||||
enhanced_topic['object'] = spot_data.get('name')
|
||||
logger.info(f"从数据库加载景区数据: {spot_data.get('name')} (ID: {spot_id})")
|
||||
enhanced_topic['object'] = spot_name
|
||||
logger.info(f"从数据库加载景区数据: {spot_name} (ID: {spot_id})")
|
||||
|
||||
# 处理产品ID
|
||||
if 'productIds' in topic and topic['productIds']:
|
||||
product_id = topic['productIds'][0] if isinstance(topic['productIds'], list) else topic['productIds']
|
||||
product_data = db_service.get_product_by_id(product_id)
|
||||
if product_data:
|
||||
product_name = product_data.get('productName')
|
||||
enhanced_topic['product_object'] = product_data
|
||||
enhanced_topic['product'] = product_data.get('productName')
|
||||
logger.info(f"从数据库加载产品数据: {product_data.get('productName')} (ID: {product_id})")
|
||||
enhanced_topic['product'] = product_name
|
||||
logger.info(f"从数据库加载产品数据: {product_name} (ID: {product_id})")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"增强选题数据时发生错误: {e}", exc_info=True)
|
||||
|
||||
return enhanced_topic
|
||||
|
||||
async def _enhance_with_related_ids(self, enhanced_topic: Dict[str, Any], db_service, related_ids: Dict[str, List[str]]) -> Dict[str, Any]:
|
||||
"""
|
||||
使用related_object_ids中的反射ID进行数据库查询和增强
|
||||
|
||||
Args:
|
||||
enhanced_topic: 待增强的选题数据
|
||||
db_service: 数据库服务实例
|
||||
related_ids: 反射的ID字典
|
||||
|
||||
Returns:
|
||||
增强后的选题数据
|
||||
"""
|
||||
logger.info("开始使用反射ID进行选题数据增强")
|
||||
|
||||
try:
|
||||
# 处理风格ID
|
||||
style_ids = related_ids.get('style_ids', [])
|
||||
if style_ids:
|
||||
style_id = int(style_ids[0]) # 取第一个ID
|
||||
style_data = db_service.get_style_by_id(style_id)
|
||||
if style_data:
|
||||
enhanced_topic['style_object'] = style_data
|
||||
enhanced_topic['style'] = style_data.get('styleName')
|
||||
logger.info(f"通过反射ID加载风格数据: {style_data.get('styleName')} (ID: {style_id})")
|
||||
|
||||
# 处理受众ID
|
||||
audience_ids = related_ids.get('audience_ids', [])
|
||||
if audience_ids:
|
||||
audience_id = int(audience_ids[0]) # 取第一个ID
|
||||
audience_data = db_service.get_audience_by_id(audience_id)
|
||||
if audience_data:
|
||||
enhanced_topic['audience_object'] = audience_data
|
||||
enhanced_topic['targetAudience'] = audience_data.get('audienceName')
|
||||
logger.info(f"通过反射ID加载受众数据: {audience_data.get('audienceName')} (ID: {audience_id})")
|
||||
|
||||
# 处理景区ID
|
||||
scenic_spot_ids = related_ids.get('scenic_spot_ids', [])
|
||||
if scenic_spot_ids:
|
||||
spot_id = int(scenic_spot_ids[0]) # 取第一个ID
|
||||
spot_data = db_service.get_scenic_spot_by_id(spot_id)
|
||||
if spot_data:
|
||||
enhanced_topic['scenic_spot_object'] = spot_data
|
||||
enhanced_topic['object'] = spot_data.get('name')
|
||||
logger.info(f"通过反射ID加载景区数据: {spot_data.get('name')} (ID: {spot_id})")
|
||||
|
||||
# 处理产品ID
|
||||
product_ids = related_ids.get('product_ids', [])
|
||||
if product_ids:
|
||||
product_id = int(product_ids[0]) # 取第一个ID
|
||||
product_data = db_service.get_product_by_id(product_id)
|
||||
if product_data:
|
||||
enhanced_topic['product_object'] = product_data
|
||||
enhanced_topic['product'] = product_data.get('productName')
|
||||
logger.info(f"通过反射ID加载产品数据: {product_data.get('productName')} (ID: {product_id})")
|
||||
|
||||
logger.info("反射ID数据增强完成")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"使用反射ID增强选题数据时发生错误: {e}", exc_info=True)
|
||||
|
||||
return enhanced_topic
|
||||
|
||||
async def generate_content_with_prompt(self, topic: Dict[str, Any], system_prompt: str, user_prompt: str) -> Tuple[str, str, Dict[str, Any]]:
|
||||
"""
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"host": "localhost",
|
||||
"user": "root",
|
||||
"password": "civmek-rezTed-0hovre",
|
||||
"password": "Kj#9mP2$",
|
||||
"database": "travel_content",
|
||||
"port": 3306,
|
||||
"charset": "utf8mb4",
|
||||
|
||||
@ -32,7 +32,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=int(os.getenv("PORT", 8000)),
|
||||
default=int(os.getenv("PORT", 2714)),
|
||||
help="监听端口",
|
||||
)
|
||||
parser.add_argument(
|
||||
|
||||
711
海报Fabric.js前端对接文档.md
Normal file
711
海报Fabric.js前端对接文档.md
Normal file
@ -0,0 +1,711 @@
|
||||
# 海报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)
|
||||
Loading…
x
Reference in New Issue
Block a user