""" 仪表盘设计节点 提供可视化设计界面,让用户设计仪表盘布局 """ import ast import json import logging import random from typing import Any, Dict, List, Optional from uuid import uuid4 from ..base import BaseNode, NodeContext, NodeResult from ..registry import NodeRegistry logger = logging.getLogger(__name__) @NodeRegistry.register class DashboardDesignNode(BaseNode): """ 仪表盘设计节点 提供可视化设计界面,让用户设计仪表盘布局和组件 """ node_type = 'dashboard_design' node_name = '仪表盘设计' node_category = 'dashboard' node_icon = 'layout-dashboard' node_description = '可视化设计仪表盘布局和组件' inputs = [ { 'name': 'dashboard_code', 'type': 'string', 'description': '仪表盘编码(来自基础信息节点)', 'required': False, }, { 'name': 'design_suggestion', 'type': 'string', 'description': 'LLM生成的设计建议(可选)', 'required': False, }, { 'name': 'initial_config', 'type': 'object', 'description': '初始页面配置(可选)', 'required': False, }, ] outputs = [ { 'name': 'page_config', 'type': 'object', 'description': '页面设计配置(dashboard-design的JSON配置)', }, { 'name': 'design_title', 'type': 'string', 'description': '设计方案标题', }, { 'name': 'confirmed', 'type': 'boolean', 'description': '是否已确认', }, ] def execute(self, context: NodeContext) -> NodeResult: """执行节点""" try: # 检查是否有用户提交的设计配置 user_input = context.variables.get('__user_input__') if user_input: design_data = self._parse_user_design(user_input) if design_data: logger.info(f'DashboardDesignNode - 使用用户设计的配置') context.variables.pop('__user_input__', None) return self._build_result(design_data, confirmed=True) # 获取输入参数 dashboard_code = context.resolve_template(self.config.get('dashboard_code', '')) design_suggestion = context.resolve_template(self.config.get('design_suggestion', '')) initial_config = self.config.get('initial_config', {}) # 如果initial_config是变量引用,解析它 if isinstance(initial_config, str) and initial_config.startswith('{{'): initial_config = self._resolve_variable_value(context, initial_config) or {} logger.info(f'DashboardDesignNode - dashboard_code={dashboard_code}') # 检查是否需要确认(支持布尔值、字符串模式和变量引用) require_confirmation = self.resolve_require_confirmation(context, default=True) design_title = self.config.get('design_title', '仪表盘设计') # 如果没有初始配置,根据 design_suggestion 生成默认模板 if not initial_config or not initial_config.get('widgets'): initial_config = self._generate_dashboard_template(design_suggestion, dashboard_code) # 构建设计数据 design_data = { 'dashboard_code': dashboard_code, 'design_title': design_title, 'design_suggestion': design_suggestion, 'page_config': initial_config, } if require_confirmation: return NodeResult( success=True, output={ 'page_config': design_data.get('page_config', {}), 'design_title': design_title, 'confirmed': False, }, waiting_for_input=True, waiting_config={ 'type': 'design_preview', 'preview_type': 'dashboard_design', 'title': design_title, 'data': design_data, 'editable': True, 'fullscreen': True, }, ) return self._build_result(design_data, confirmed=True) except Exception as e: logger.error(f'DashboardDesignNode - 执行失败: {str(e)}') return NodeResult( success=False, error=f'仪表盘设计节点执行失败: {str(e)}', ) def _parse_user_design(self, user_input: Any) -> Optional[Dict[str, Any]]: """解析用户提交的设计配置""" try: if isinstance(user_input, str): data = json.loads(user_input) else: data = user_input if isinstance(data, dict): return data return None except Exception as e: logger.error(f'解析用户设计配置失败: {e}') return None def _build_result(self, design_data: Dict[str, Any], confirmed: bool) -> NodeResult: """构建节点结果""" page_config = design_data.get('page_config', {}) design_title = design_data.get('design_title', '仪表盘设计') output_data = { 'page_config': page_config, 'design_title': design_title, 'confirmed': confirmed, } return NodeResult( success=True, output=output_data, output_variables=output_data, # 添加 output_variables ) def _resolve_variable_value(self, context: NodeContext, value: Any) -> Any: """解析变量引用""" if isinstance(value, str): resolved = context.resolve_template(value) if resolved != value: try: return json.loads(resolved) if isinstance(resolved, str) else resolved except (json.JSONDecodeError, TypeError): return resolved return value def _generate_dashboard_template(self, design_suggestion: str, dashboard_code: str) -> Dict[str, Any]: """根据 LLM 建议生成仪表盘模板""" try: # 解析 design_suggestion(可能是字符串形式的字典) modules = self._parse_design_suggestion(design_suggestion) # 生成基础模板 template = { "id": str(uuid4()), "name": "我的仪表盘", "columns": 12, "rowHeight": 50, "margin": [12, 12], "widgets": [] } # 添加欢迎卡片 welcome_id = str(uuid4()) template["widgets"].append({ "id": welcome_id, "i": welcome_id, "type": "welcome-card", "x": 0, "y": 0, "w": 8, "h": 2, "minW": 4, "minH": 2, "title": "欢迎卡片", "props": { "title": "欢迎回来", "subtitle": "今天是个好日子", "showTime": True, "showWeather": False } }) # 添加天气组件 weather_id = str(uuid4()) template["widgets"].append({ "id": weather_id, "i": weather_id, "type": "weather", "x": 8, "y": 0, "w": 4, "h": 2, "minW": 2, "minH": 2, "title": "天气", "props": { "title": "今日天气", "city": "北京", "temperature": 25, "weather": "晴", "humidity": 45, "wind": "东北风 3级", "icon": "sunny" } }) # 添加日历 calendar_id = str(uuid4()) template["widgets"].append({ "id": calendar_id, "i": calendar_id, "type": "calendar", "x": 8, "y": 2, "w": 4, "h": 5, "minW": 3, "minH": 4, "title": "日历", "props": { "title": "日历", "showLunar": False } }) # 根据模块生成图表 y_offset = 2 if modules and len(modules) > 0: # 生成折线图(基于第一个模块) module1 = modules[0] if len(modules) > 0 else {"module_name": "数据", "moduld_fields": []} line_chart = self._generate_line_chart(module1, 0, y_offset) template["widgets"].append(line_chart) y_offset += 5 # 生成柱状图(基于第二个模块) module2 = modules[1] if len(modules) > 1 else module1 bar_chart = self._generate_bar_chart(module2, 0, y_offset) template["widgets"].append(bar_chart) # 生成饼图(基于第三个模块) module3 = modules[2] if len(modules) > 2 else module1 pie_chart = self._generate_pie_chart(module3, 8, y_offset) template["widgets"].append(pie_chart) else: # 没有模块信息,使用默认图表 template["widgets"].extend([ self._generate_line_chart({"module_name": "访问趋势", "moduld_fields": []}, 0, y_offset), self._generate_bar_chart({"module_name": "销售统计", "moduld_fields": []}, 0, y_offset + 5), self._generate_pie_chart({"module_name": "流量来源", "moduld_fields": []}, 8, y_offset + 5) ]) return template except Exception as e: logger.error(f'生成仪表盘模板失败: {e}') # 返回空模板 return { "id": str(uuid4()), "name": "我的仪表盘", "columns": 12, "rowHeight": 50, "margin": [12, 12], "widgets": [] } def _parse_design_suggestion(self, design_suggestion: str) -> List[Dict[str, Any]]: """解析 LLM 生成的设计建议""" try: if not design_suggestion: return [] # 尝试解析为 JSON if isinstance(design_suggestion, str): try: data = json.loads(design_suggestion) except json.JSONDecodeError: # 尝试使用 ast.literal_eval try: data = ast.literal_eval(design_suggestion) except (ValueError, SyntaxError): return [] else: data = design_suggestion # 提取模块信息 if isinstance(data, dict): modules = data.get('app_modules', []) if isinstance(modules, list): return modules return [] except Exception as e: logger.error(f'解析设计建议失败: {e}') return [] def _generate_line_chart(self, module: Dict[str, Any], x: int, y: int) -> Dict[str, Any]: """生成折线图""" module_name = module.get('module_name', '访问趋势') fields = module.get('moduld_fields', []) # 生成模拟数据 months = ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"] series_data = [] # 根据字段生成系列(最多2个) for i, field in enumerate(fields[:2]): data = [random.randint(500, 1500) for _ in range(12)] series_data.append({ "name": field if isinstance(field, str) else f"系列{i+1}", "data": data }) # 如果没有字段,使用默认系列 if not series_data: series_data = [ {"name": "访问量", "data": [820, 932, 901, 934, 1290, 1330, 1320, 1450, 1200, 1100, 1350, 1500]}, {"name": "下载量", "data": [620, 732, 701, 734, 1090, 1130, 1120, 1250, 1000, 900, 1150, 1300]} ] line_id = str(uuid4()) return { "id": line_id, "i": line_id, "type": "chart-line", "x": x, "y": y, "w": 8, "h": 5, "minW": 4, "minH": 3, "title": "折线图", "props": { "title": module_name, "smooth": True, "showArea": False, "showSymbol": True, "symbolSize": 6, "lineWidth": 2, "showLegend": True, "legendPosition": "top", "colors": ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de"], "xAxisData": months, "seriesData": series_data } } def _generate_bar_chart(self, module: Dict[str, Any], x: int, y: int) -> Dict[str, Any]: """生成柱状图""" module_name = module.get('module_name', '销售统计') fields = module.get('moduld_fields', []) # 生成模拟数据 regions = ["华东", "华南", "华北", "华中", "西南", "西北", "东北"] series_data = [] # 根据字段生成系列(最多2个) for i, field in enumerate(fields[:2]): data = [random.randint(200, 500) for _ in range(7)] series_data.append({ "name": field if isinstance(field, str) else f"{2023+i}", "data": data }) # 如果没有字段,使用默认系列 if not series_data: series_data = [ {"name": "2023", "data": [320, 302, 301, 334, 390, 330, 320]}, {"name": "2024", "data": [420, 382, 391, 434, 490, 430, 420]} ] bar_id = str(uuid4()) return { "id": bar_id, "i": bar_id, "type": "chart-bar", "x": x, "y": y, "w": 8, "h": 6, "minW": 4, "minH": 3, "title": "柱状图", "props": { "title": module_name, "horizontal": False, "stack": False, "barWidth": "auto", "barRadius": 4, "showBackground": False, "showLegend": True, "legendPosition": "top", "colors": ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de"], "xAxisData": regions, "seriesData": series_data } } def _generate_pie_chart(self, module: Dict[str, Any], x: int, y: int) -> Dict[str, Any]: """生成饼图""" module_name = module.get('module_name', '流量来源') fields = module.get('moduld_fields', []) # 根据字段生成数据 series_data = [] if fields and len(fields) > 0: # 使用字段名作为分类(最多5个) for field in fields[:5]: series_data.append({ "name": field if isinstance(field, str) else f"分类{len(series_data)+1}", "value": random.randint(300, 1000) }) # 如果没有字段,使用默认数据 if not series_data: series_data = [ {"name": "搜索引擎", "value": 1048}, {"name": "直接访问", "value": 735}, {"name": "邮件营销", "value": 580}, {"name": "联盟广告", "value": 484}, {"name": "视频广告", "value": 300} ] pie_id = str(uuid4()) return { "id": pie_id, "i": pie_id, "type": "chart-pie", "x": x, "y": y, "w": 4, "h": 6, "minW": 3, "minH": 3, "title": "饼图", "props": { "title": module_name, "pieType": "rose", "radius": ["0%", "70%"], "showLabel": True, "labelPosition": "outside", "showLegend": True, "legendPosition": "bottom", "colors": ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de"], "seriesData": series_data } }