""" @file generate_layout.py @brief Vue组件代码生成模块 基于DeepSeek API生成响应式Vue 3组件布局代码,支持预定义模板 @author 王秀强 (2310460@mail.nankai.edu.cn) @date 2025.5.19 @version v2.0.0 @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, # 增加token数量 stream=False, max_retries=3, ): """ 调用 DeepSeek API,支持多轮对话和流式/非流式响应 """ # 初始化 OpenAI 客户端 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: print(f"{Fore.BLUE}📡 正在调用DeepSeek API (尝试 {attempt + 1}/{max_retries})...{Style.RESET_ALL}") response = client.chat.completions.create( model=model, messages=messages, temperature=temperature, max_tokens=max_tokens, stream=stream ) # 流式响应 if stream: def stream_generator(): usage = None for chunk in response: if chunk.choices[0].delta.content is not None: yield chunk.choices[0].delta.content return usage return stream_generator(), None else: # 非流式响应 content = response.choices[0].message.content print(f"{Fore.GREEN}✅ API调用成功,返回内容长度: {len(content)}{Style.RESET_ALL}") return content, response.usage except Exception as e: print(f"{Fore.RED}❌ API调用失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}{Style.RESET_ALL}") if hasattr(e, 'status_code') and e.status_code == 429: # 限流 print(f"{Fore.YELLOW}⏳ 请求过于频繁,等待重试...{Style.RESET_ALL}") time.sleep(2 ** attempt) # 指数退避 elif attempt == max_retries - 1: raise else: time.sleep(1) raise Exception("达到最大重试次数,API 调用失败") def load_vue_templates() -> Dict: """加载预定义的Vue模板配置""" template_path = "../configs/vue_templates.yaml" try: with open(template_path, 'r', encoding='utf-8') as f: templates = yaml.safe_load(f) print(f"{Fore.GREEN}✅ Vue模板配置加载成功{Style.RESET_ALL}") return templates.get('vue_templates', {}) except Exception as e: print(f"{Fore.YELLOW}⚠️ Vue模板配置加载失败: {e},使用默认模板{Style.RESET_ALL}") return {} def get_template_by_images(parse_imglist: List[Dict]) -> Optional[str]: """ 根据图片列表选择合适的预定义模板 参数: parse_imglist: 图片信息列表 返回: 选中的模板代码,如果没有匹配的模板则返回None """ templates = load_vue_templates() if not templates or not parse_imglist: return None # 检查固定的图片文件 fixed_images = ["lotus.jpg", "nku.png", "stamp.jpg", "background.png"] for img_name in fixed_images: img_path = f"../outputs/{img_name}" if os.path.exists(img_path) and img_name in templates: print(f"{Fore.CYAN}📋 找到匹配的预定义模板: {img_name}{Style.RESET_ALL}") return templates[img_name]['template'] print(f"{Fore.YELLOW}⚠️ 未找到匹配的预定义模板,将使用AI生成{Style.RESET_ALL}") return None def generate_layout_prompt(user_input_analysis_result: Dict, parse_imglist: List[Dict], suggestions: Dict = None) -> str: """ 生成增强的Vue布局提示,包含文案内容 这个函数从run_pipeline.py移动到这里 """ width = user_input_analysis_result.get("width", 1080) height = user_input_analysis_result.get("height", 1920) theme = user_input_analysis_result.get("main_theme", "活动海报") # 构造图片信息字符串 images_info = "\n".join( [f"- {img['picture_name']} ({img['picture_description']})" for img in parse_imglist] ) # 构造文案信息 content_info = "" if suggestions: try: if 'layer6_title_content' in suggestions: title = suggestions['layer6_title_content'].get('content', theme) content_info += f"- 主标题: {title}\n" if 'layer7_subtitle_content' in suggestions: subtitle = suggestions['layer7_subtitle_content'].get('content', '精彩活动,敬请参与') content_info += f"- 副标题: {subtitle}\n" if 'layer5_logo_content' in suggestions: logo = suggestions['layer5_logo_content'].get('text', '主办方') content_info += f"- Logo文字: {logo}\n" except Exception as e: print(f"{Fore.YELLOW}⚠️ 文案信息解析错误: {e}{Style.RESET_ALL}") content_info = f"- 主标题: {theme}\n- 副标题: 精彩活动,敬请参与\n" # 调用DeepSeek生成动态排版Prompt system_prompt = "你是一个擅长前端开发的AI,专注于生成Vue.js代码。请根据提供的信息生成完整的Vue组件,包含所有必要的HTML结构和基础定位样式。" prompt = f""" 请生成一个Vue.js组件代码,用于{theme}海报,要求如下: 组件尺寸: {width}x{height}px 图片资源: {images_info} 文案内容: {content_info} 布局要求: 1. 背景图层: 使用第一张图片作为背景,占据整个组件区域 2. 主标题: 位于画布上方1/3处,居中显示 3. 副标题: 位于主标题下方,居中显示 4. 内容区域: 使用剩余图片,合理布局在中间区域 5. Logo区域: 位于底部,居中显示 技术要求: - 使用Vue 3 Composition API - 使用absolute定位进行精确布局 - 包含完整的template、script和style部分 - 确保所有文本内容都正确显示 - 图片使用相对路径引用 请生成完整可用的Vue组件代码,不要包含任何说明文字。 """ try: result, _ = call_deepseek(prompt=prompt, system_prompt=system_prompt, temperature=0.4) return result except Exception as e: print(f"{Fore.RED}❌ 布局提示生成失败: {e}{Style.RESET_ALL}") return generate_fallback_vue_code(theme, width, height) def fill_template_content(template: str, suggestions: Dict) -> str: """ 填充模板中的动态内容 参数: template: Vue模板代码 suggestions: 文案建议 返回: 填充后的Vue代码 """ filled_template = template try: # 提取文案内容 title_content = suggestions.get('layer6_title_content', {}).get('content', '默认标题') subtitle_content = suggestions.get('layer7_subtitle_content', {}).get('content', '默认副标题') logo_content = suggestions.get('layer5_logo_content', {}).get('text', '主办方') # 替换模板占位符 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) print(f"{Fore.GREEN}✅ 模板内容填充完成{Style.RESET_ALL}") except Exception as e: print(f"{Fore.YELLOW}⚠️ 模板填充出错: {e},使用默认内容{Style.RESET_ALL}") return filled_template def generate_vue_code_enhanced( user_input_analysis_result: Dict, parse_imglist: List[Dict], suggestions: Dict = None, prompt: str = None ) -> str: """ 增强的Vue代码生成函数 优先使用预定义模板,如果没有匹配的模板则使用AI生成 参数: user_input_analysis_result: 用户输入分析结果 parse_imglist: 图片信息列表 suggestions: 文案建议 prompt: 自定义提示词(可选) 返回: Vue组件代码 """ print(f"{Fore.CYAN}🎨 开始增强Vue代码生成...{Style.RESET_ALL}") # 1. 尝试使用预定义模板 template_code = get_template_by_images(parse_imglist) if template_code and suggestions: print(f"{Fore.GREEN}✅ 使用预定义模板生成Vue代码{Style.RESET_ALL}") vue_code = fill_template_content(template_code, suggestions) return vue_code # 2. 如果没有合适的模板,使用AI生成 print(f"{Fore.BLUE}🤖 使用AI生成Vue代码{Style.RESET_ALL}") if not prompt: prompt = generate_layout_prompt(user_input_analysis_result, parse_imglist, suggestions) return generate_vue_code(prompt) def generate_vue_code(prompt=None): """ 原有的Vue代码生成函数(保持兼容性) """ if not prompt: prompt = ( "生成一个Vue组件代码,用于端午节活动海报,包含以下部分并指定排版位置:" "1. 背景图层:div,占据整个组件区域。" "2. 主体图层:div,位于顶部1/4处,居中,包含标题和副标题。" "3. 活动亮点:div,位于底部1/4处,居中,使用网格布局展示三项活动(每项包含图标、标题和描述)。" "4. 页脚:div,位于底部,居中,包含主办单位信息和logo图片。" "组件尺寸为1080x1920px,布局使用absolute定位,生成完整可用的Vue 3代码。" ) system_prompt = ( "你是一个擅长前端开发的AI,专注于生成Vue.js代码。" "请生成完整的Vue 3组件,包含template、script setup和style部分。" "确保代码结构清晰,语法正确,可以直接使用。" "不要包含任何解释文字,只返回纯代码。" ) try: print(f"{Fore.CYAN}🎨 正在生成Vue组件代码...{Style.RESET_ALL}") result, usage = call_deepseek(prompt=prompt, system_prompt=system_prompt, temperature=0.4) # 清理代码,移除可能的markdown标记 if result: # 移除markdown代码块标记 if "```vue" in result: result = result.split("```vue")[1].split("```")[0].strip() elif "```html" in result: result = result.split("```html")[1].split("```")[0].strip() elif result.startswith("```") and result.endswith("```"): result = result[3:-3].strip() print(f"{Fore.GREEN}✅ Vue代码生成成功,长度: {len(result)} 字符{Style.RESET_ALL}") return result else: print(f"{Fore.RED}❌ Vue代码生成失败,返回空内容{Style.RESET_ALL}") return generate_fallback_vue_code() except Exception as e: print(f"{Fore.RED}❌ Vue代码生成异常: {str(e)}{Style.RESET_ALL}") return generate_fallback_vue_code() def generate_fallback_vue_code(theme="默认主题", width=1080, height=1920): """ 生成备用的Vue代码 """ print(f"{Fore.YELLOW}⚠️ 使用备用Vue模板{Style.RESET_ALL}") return f""" """ 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) print(f"{Fore.GREEN}✅ Vue代码已保存到: {file_path}{Style.RESET_ALL}") # 验证文件是否成功创建 if os.path.exists(file_path): file_size = os.path.getsize(file_path) print(f"{Fore.CYAN}📁 文件大小: {file_size} 字节{Style.RESET_ALL}") else: print(f"{Fore.RED}❌ 文件保存失败{Style.RESET_ALL}") except Exception as e: print(f"{Fore.RED}❌ 保存代码时出错: {str(e)}{Style.RESET_ALL}") if __name__ == "__main__": print(f"{Fore.MAGENTA}🚀 开始生成Vue组件...{Style.RESET_ALL}") # 测试增强的Vue代码生成 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) print(f"{Fore.GREEN}✅ Vue组件代码生成完成{Style.RESET_ALL}")