374 lines
14 KiB
Python
374 lines
14 KiB
Python
"""
|
||
@file generate_layout.py
|
||
@brief Vue组件代码生成模块
|
||
基于DeepSeek API生成响应式Vue 3组件布局代码,支持预定义模板
|
||
|
||
@author 王秀强 (2310460@mail.nankai.edu.cn)
|
||
@date 2025.5.19
|
||
@version v0.5.2
|
||
|
||
@details
|
||
本文件主要实现:
|
||
- DeepSeek API调用封装和错误处理
|
||
- Vue 3 Composition API组件代码生成
|
||
- 海报布局的动态排版和样式生成
|
||
- 预定义Vue模板系统
|
||
- 增强Vue代码生成逻辑
|
||
- 代码清理和格式化处理
|
||
|
||
@note
|
||
- 需要配置DEEPSEEK_API_KEY环境变量
|
||
- 支持流式和非流式响应模式
|
||
- 生成的Vue代码包含完整的template、script和style部分
|
||
- 具备指数退避重试机制处理API限流
|
||
- 支持基于图片类型的预定义模板
|
||
|
||
@usage
|
||
# 生成Vue组件代码
|
||
vue_code = generate_vue_code_enhanced("生成端午节海报Vue组件", parse_imglist, suggestions)
|
||
save_code(vue_code, "../outputs/poster.vue")
|
||
|
||
@copyright
|
||
(c) 2025 砚生项目组
|
||
"""
|
||
|
||
import os
|
||
import yaml
|
||
from openai import OpenAI
|
||
from dotenv import load_dotenv
|
||
import time
|
||
from colorama import init, Fore, Back, Style
|
||
from typing import List, Dict, Optional
|
||
|
||
# 初始化colorama
|
||
init(autoreset=True)
|
||
|
||
# === Config LLM call ===
|
||
load_dotenv()
|
||
deepseek_url = 'https://api.deepseek.com/v1' # set to be compatible with the OpenAI API
|
||
deepseek_api = os.getenv("DEEPSEEK_API_KEY")
|
||
if not deepseek_api:
|
||
raise ValueError("DEEPSEEK_API_KEY not set!")
|
||
|
||
def call_deepseek(
|
||
messages=None,
|
||
system_prompt="你是一个擅长前端开发的AI,专注于生成Vue.js代码。",
|
||
prompt=None,
|
||
model='deepseek-chat',
|
||
temperature=0.6,
|
||
max_tokens=2000,
|
||
stream=False,
|
||
max_retries=3,
|
||
):
|
||
"""调用 DeepSeek API,支持多轮对话和流式/非流式响应"""
|
||
client = OpenAI(api_key=deepseek_api, base_url=deepseek_url)
|
||
|
||
# 参数验证
|
||
if messages is None:
|
||
if not prompt:
|
||
raise ValueError("必须提供 message 或 prompt 参数")
|
||
messages = [
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": prompt}
|
||
]
|
||
elif not isinstance(messages, list) or not messages:
|
||
raise ValueError("message 参数必须是非空列表")
|
||
|
||
# 模型验证
|
||
models = ["deepseek-chat", "deepseek-reasoner"]
|
||
if model not in models:
|
||
raise ValueError(f"无效的模型名称: {model},可用模型: {models}")
|
||
|
||
# 调用 API
|
||
for attempt in range(max_retries):
|
||
try:
|
||
response = client.chat.completions.create(
|
||
model=model,
|
||
messages=messages,
|
||
temperature=temperature,
|
||
max_tokens=max_tokens,
|
||
stream=stream
|
||
)
|
||
|
||
if stream:
|
||
# 流式响应处理
|
||
def stream_generator():
|
||
for chunk in response:
|
||
if hasattr(chunk, 'choices') and chunk.choices[0].delta.content is not None:
|
||
yield chunk.choices[0].delta.content
|
||
return stream_generator(), None
|
||
else:
|
||
# 非流式响应
|
||
if hasattr(response, 'choices') and response.choices:
|
||
content = response.choices[0].message.content
|
||
usage = getattr(response, 'usage', None)
|
||
return content, usage
|
||
return "", None
|
||
|
||
except Exception as e:
|
||
if hasattr(e, 'status_code') and getattr(e, 'status_code') == 429:
|
||
time.sleep(2 ** attempt) # 指数退避
|
||
elif attempt == max_retries - 1:
|
||
raise
|
||
else:
|
||
time.sleep(1)
|
||
|
||
raise Exception("达到最大重试次数,API 调用失败")
|
||
|
||
|
||
def load_vue_templates() -> Dict:
|
||
"""加载预定义的Vue模板配置"""
|
||
from utils import load_vue_templates as utils_load_templates
|
||
return utils_load_templates()
|
||
|
||
|
||
def get_template_by_images(parse_imglist: List[Dict]) -> Optional[str]:
|
||
"""根据图片列表选择合适的预定义模板"""
|
||
templates = load_vue_templates()
|
||
|
||
if not templates or not parse_imglist:
|
||
return None
|
||
|
||
# 使用utils中的函数确定模板类型
|
||
from utils import determine_template_type as utils_determine_type
|
||
template_choice = utils_determine_type(parse_imglist)
|
||
|
||
if template_choice in templates:
|
||
return templates[template_choice]['template']
|
||
|
||
# 备用:检查特定文件名
|
||
for img_info in parse_imglist:
|
||
img_name = img_info.get('picture_name', '')
|
||
if img_name in templates:
|
||
return templates[img_name]['template']
|
||
|
||
return None
|
||
|
||
|
||
def determine_template_type(parse_imglist: List[Dict]) -> str:
|
||
"""根据图片信息确定模板类型"""
|
||
from utils import determine_template_type as utils_determine_type
|
||
return utils_determine_type(parse_imglist)
|
||
|
||
|
||
def generate_layout_prompt(user_input_analysis_result: Dict, parse_imglist: List[Dict], suggestions: Optional[Dict] = None) -> str:
|
||
"""根据用户分析结果动态生成Vue布局提示"""
|
||
width = user_input_analysis_result.get("width", 1080)
|
||
height = user_input_analysis_result.get("height", 1920)
|
||
theme = user_input_analysis_result.get("main_theme", "活动海报")
|
||
|
||
# 从用户分析中提取更多信息
|
||
style = user_input_analysis_result.get("style", "现代简约")
|
||
color_scheme = user_input_analysis_result.get("color_scheme", "默认配色")
|
||
layout_type = user_input_analysis_result.get("layout_type", "标准布局")
|
||
target_audience = user_input_analysis_result.get("target_audience", "一般用户")
|
||
|
||
# 构造图片信息
|
||
images_info = []
|
||
for i, img in enumerate(parse_imglist):
|
||
img_desc = f"图片{i+1}: {img['picture_name']} - {img['picture_description']}"
|
||
images_info.append(img_desc)
|
||
|
||
images_text = "\n".join(images_info) if images_info else "无特定图片资源"
|
||
|
||
# 构造文案信息
|
||
content_parts = []
|
||
if suggestions:
|
||
try:
|
||
if 'layer6_title_content' in suggestions:
|
||
title = suggestions['layer6_title_content'].get('content', theme)
|
||
content_parts.append(f"主标题: {title}")
|
||
|
||
if 'layer7_subtitle_content' in suggestions:
|
||
subtitle = suggestions['layer7_subtitle_content'].get('content', '精彩活动,敬请参与')
|
||
content_parts.append(f"副标题: {subtitle}")
|
||
|
||
if 'layer5_logo_content' in suggestions:
|
||
logo = suggestions['layer5_logo_content'].get('text', '')
|
||
if logo:
|
||
content_parts.append(f"Logo文字: {logo}")
|
||
except Exception:
|
||
content_parts = [f"主标题: {theme}", "副标题: 精彩活动,敬请参与"]
|
||
else:
|
||
content_parts = [f"主标题: {theme}", "副标题: 精彩活动,敬请参与"]
|
||
|
||
content_info = "\n".join([f"- {part}" for part in content_parts])
|
||
|
||
# 根据主题类型调整布局描述
|
||
layout_requirements = []
|
||
if "节日" in theme or "festival" in theme.lower():
|
||
layout_requirements = [
|
||
"背景图层: 使用节日主题背景,营造氛围",
|
||
"标题区域: 突出节日名称,位于画布上方,使用醒目字体",
|
||
"装饰元素: 添加节日相关装饰图案,分布在画面周围",
|
||
"日期信息: 在适当位置显示日期",
|
||
"Logo区域: 位于底部,展示主办方信息"
|
||
]
|
||
elif "校园" in theme or "大学" in theme or "学术" in theme:
|
||
layout_requirements = [
|
||
"背景图层: 使用校园或学术主题背景",
|
||
"标题区域: 学术风格标题,位于画布上方1/3处",
|
||
"内容区域: 展示学术内容或校园风光,居中布局",
|
||
"信息栏: 显示相关学术信息或活动详情",
|
||
"Logo区域: 展示学校标识,位于底部"
|
||
]
|
||
else:
|
||
layout_requirements = [
|
||
"背景图层: 使用主题相关背景图片,占据整个组件区域",
|
||
"主标题: 位于画布上方1/3处,居中显示,突出主题",
|
||
"副标题: 位于主标题下方,居中显示,补充说明",
|
||
"内容区域: 合理布局图片和信息,保持视觉平衡",
|
||
"Logo区域: 位于底部,居中显示主办方信息"
|
||
]
|
||
|
||
layout_text = "\n".join([f"{i+1}. {req}" for i, req in enumerate(layout_requirements)])
|
||
|
||
system_prompt = f"你是一个专业的前端设计师,擅长根据用户需求生成{style}风格的Vue.js组件。请根据{target_audience}的审美偏好,生成完整的Vue组件代码。"
|
||
|
||
prompt = f"""
|
||
请为"{theme}"主题设计一个Vue 3组件,要求如下:
|
||
|
||
【基本信息】
|
||
- 组件尺寸: {width}x{height}px
|
||
- 设计风格: {style}
|
||
- 配色方案: {color_scheme}
|
||
- 布局类型: {layout_type}
|
||
|
||
【图片资源】
|
||
{images_text}
|
||
|
||
【文案内容】
|
||
{content_info}
|
||
|
||
【布局要求】
|
||
{layout_text}
|
||
|
||
【技术要求】
|
||
- 使用Vue 3 Composition API
|
||
- 使用absolute或flex定位进行精确布局
|
||
- 包含完整的template、script setup和style部分
|
||
- 确保响应式设计和良好的视觉效果
|
||
- 图片使用相对路径引用
|
||
- 样式要体现{style}的设计理念
|
||
|
||
请生成完整可用的Vue组件代码,代码要简洁优雅,符合{style}风格特点。
|
||
"""
|
||
|
||
try:
|
||
result, _ = call_deepseek(prompt=prompt, system_prompt=system_prompt, temperature=0.4)
|
||
return result if isinstance(result, str) else ""
|
||
except Exception:
|
||
return ""
|
||
|
||
|
||
def fill_template_content(template: str, suggestions: Dict) -> str:
|
||
"""填充模板中的动态内容"""
|
||
from utils import extract_content_from_suggestions
|
||
|
||
try:
|
||
title_content, subtitle_content, logo_content = extract_content_from_suggestions(suggestions)
|
||
|
||
filled_template = template
|
||
|
||
# 根据模板类型进行替换
|
||
if 'illustration-theme' in template:
|
||
filled_template = filled_template.replace("'见微知著记录南开'", f"'{title_content}'")
|
||
filled_template = filled_template.replace("'南开大学融媒体中心'", f"'{subtitle_content}'")
|
||
if logo_content:
|
||
filled_template = filled_template.replace("'主办方'", f"'{logo_content}'")
|
||
|
||
elif 'festival-theme' in template:
|
||
filled_template = filled_template.replace("'南开大学'", "'南开大学'")
|
||
filled_template = filled_template.replace("'中秋节'", f"'{title_content}'")
|
||
filled_template = filled_template.replace("'月圆人团圆'", f"'{subtitle_content}'")
|
||
from datetime import datetime
|
||
current_date = datetime.now().strftime('%Y年%m月%d日')
|
||
filled_template = filled_template.replace("'2025年10月6日'", f"'{current_date}'")
|
||
|
||
else:
|
||
# 通用替换
|
||
filled_template = filled_template.replace('{{ title_content }}', title_content)
|
||
filled_template = filled_template.replace('{{ subtitle_content }}', subtitle_content)
|
||
filled_template = filled_template.replace('{{ logo_content }}', logo_content)
|
||
|
||
return filled_template
|
||
|
||
except Exception:
|
||
return template
|
||
|
||
|
||
def generate_vue_code_enhanced(
|
||
user_input_analysis_result: Dict,
|
||
parse_imglist: List[Dict],
|
||
suggestions: Optional[Dict] = None,
|
||
prompt: Optional[str] = None
|
||
) -> str:
|
||
"""增强的Vue代码生成函数,优先使用预定义模板"""
|
||
|
||
# 尝试使用预定义模板
|
||
template_code = get_template_by_images(parse_imglist)
|
||
|
||
if template_code and suggestions:
|
||
vue_code = fill_template_content(template_code, suggestions)
|
||
return vue_code
|
||
|
||
# 使用AI生成
|
||
if not prompt:
|
||
prompt = generate_layout_prompt(user_input_analysis_result, parse_imglist, suggestions)
|
||
|
||
return generate_vue_code(prompt)
|
||
|
||
|
||
def generate_vue_code(prompt):
|
||
"""Vue代码生成函数"""
|
||
system_prompt = (
|
||
"你是一个擅长前端开发的AI,专注于生成Vue.js代码。"
|
||
"请生成完整的Vue 3组件,包含template、script setup和style部分。"
|
||
"确保代码结构清晰,语法正确,可以直接使用。"
|
||
"不要包含任何解释文字,只返回纯代码。"
|
||
)
|
||
|
||
try:
|
||
result, usage = call_deepseek(prompt=prompt, system_prompt=system_prompt, temperature=0.4)
|
||
|
||
# 使用utils中的代码清理函数
|
||
from utils import clean_vue_code
|
||
if result and isinstance(result, str):
|
||
cleaned_result = clean_vue_code(result)
|
||
if cleaned_result:
|
||
return cleaned_result
|
||
|
||
# 如果API失败,返回简单的错误提示
|
||
return "<template><div>Vue代码生成失败</div></template>"
|
||
|
||
except Exception:
|
||
return "<template><div>Vue代码生成失败</div></template>"
|
||
|
||
def save_code(code, file_path="../outputs/generated_code.vue"):
|
||
"""保存代码到文件"""
|
||
try:
|
||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||
with open(file_path, "w", encoding="utf-8") as f:
|
||
f.write(code)
|
||
|
||
if os.path.exists(file_path):
|
||
file_size = os.path.getsize(file_path)
|
||
except Exception:
|
||
pass
|
||
|
||
if __name__ == "__main__":
|
||
# 简单测试
|
||
test_analysis = {"width": 1080, "height": 1920, "main_theme": "端午节海报"}
|
||
test_imglist = [
|
||
{"picture_name": "background", "picture_description": "背景图片"},
|
||
{"picture_name": "lotus", "picture_description": "荷花装饰"}
|
||
]
|
||
test_suggestions = {
|
||
"layer6_title_content": {"content": "端午安康"},
|
||
"layer7_subtitle_content": {"content": "粽叶飘香,龙舟竞渡"},
|
||
"layer5_logo_content": {"text": "主办方"}
|
||
}
|
||
|
||
vue_code = generate_vue_code_enhanced(test_analysis, test_imglist, test_suggestions)
|
||
save_code(vue_code)
|