""" Table to Image Renderer - Premium Black Gold Card Design Renders task report as a luxurious black-gold card for DingTalk. """ import os from typing import Dict, List, Optional, Tuple from datetime import datetime try: import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from matplotlib.patches import FancyBboxPatch, Rectangle import matplotlib.colors as mcolors HAS_MATPLOTLIB = True except ImportError: HAS_MATPLOTLIB = False class TableImageRenderer: """Render task report as a premium black-gold card.""" # Luxurious black-gold color scheme COLORS = { 'bg': '#0a0a0a', # Deep black 'card': '#121212', # Rich black 'gold': '#d4af37', # Classic gold 'gold_light': '#f4d03f', # Bright gold 'gold_dark': '#b8860b', # Dark gold 'border': '#c9a227', # Gold border 'text': '#ffffff', # White text 'text_gold': '#d4af37', # Gold text 'muted': '#888888', # Muted gray 'row_alt': '#1a1a1a', # Alternating row 'accent_red': '#ff6b6b', # Urgent red 'accent_orange': '#ffa500', # Warning orange } def __init__(self, output_dir: str = "./data"): self.output_dir = output_dir os.makedirs(output_dir, exist_ok=True) def render_task_report( self, summary: Dict, user_roles: Dict[str, str] = None, dashboard_url: str = None, switches: Dict[str, bool] = None ) -> Tuple[Optional[str], List[Dict]]: """Render task summary as a premium black-gold card.""" if not HAS_MATPLOTLIB: print("matplotlib not installed. Run: pip install matplotlib") return None, [] user_roles = user_roles or {} switches = switches or {} all_tasks = self._collect_tasks(summary, user_roles) if not all_tasks: print("No tasks to render") return None, all_tasks timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = os.path.join(self.output_dir, f"report_{timestamp}.png") try: self._render_premium_card(summary, all_tasks, output_path) print(f"Report image generated: {output_path}") return output_path, all_tasks except Exception as e: print(f"Failed to render image: {e}") import traceback traceback.print_exc() return None, all_tasks def _collect_tasks(self, summary: Dict, user_roles: Dict[str, str]) -> List[Dict]: """Collect active tasks.""" urgent, dev, design = [], [], [] for user, data in summary.get('by_user', {}).items(): role = user_roles.get(user, '其他') for t in data.get('tasks', []): status = t.get('status', '') priority = t.get('priority', '') is_urgent = priority in ['紧急', '非常紧急', 'Urgent', 'Very Urgent'] is_active = status in ['进行中', 'In Progress', 'Doing'] is_done = status in ['已完成', '关闭', '已关闭', 'Done', 'Closed'] is_overdue = t.get('is_overdue', False) if is_done or (not is_active and not is_overdue and not is_urgent): continue task = { 'executor': user, 'title': t.get('title', '')[:24] + ('...' if len(t.get('title', '')) > 24 else ''), 'due': (t.get('due', '') or '')[:10], 'is_urgent': is_urgent, 'is_overdue': is_overdue, 'task_id': t.get('id'), } if is_urgent: urgent.append(task) elif role in ['客户端', '服务端']: dev.append(task) else: design.append(task) return urgent + dev + design def _render_premium_card(self, summary: Dict, tasks: List[Dict], output_path: str): """Render premium black-gold card.""" num_tasks = len(tasks) fig_width = 5.5 fig_height = max(2.8, 1.4 + num_tasks * 0.38) fig, ax = plt.subplots(figsize=(fig_width, fig_height)) fig.patch.set_facecolor(self.COLORS['bg']) ax.set_facecolor(self.COLORS['card']) ax.set_xlim(0, 10) ax.set_ylim(0, num_tasks + 2.8) ax.axis('off') # Premium card with gold border outer_border = FancyBboxPatch((0.15, 0.15), 9.7, num_tasks + 2.5, boxstyle="round,pad=0.02,rounding_size=0.15", facecolor='none', edgecolor=self.COLORS['gold'], linewidth=2.5) ax.add_patch(outer_border) # Inner card inner_card = FancyBboxPatch((0.25, 0.25), 9.5, num_tasks + 2.3, boxstyle="round,pad=0.02,rounding_size=0.12", facecolor=self.COLORS['card'], edgecolor=self.COLORS['gold_dark'], linewidth=1) ax.add_patch(inner_card) # Premium title with decorative elements date_str = summary.get('updated_at', '')[:10] y_title = num_tasks + 2.1 # Decorative lines ax.plot([1.5, 3.8], [y_title, y_title], color=self.COLORS['gold'], linewidth=1, alpha=0.6) ax.plot([6.2, 8.5], [y_title, y_title], color=self.COLORS['gold'], linewidth=1, alpha=0.6) # Title ax.text(5, y_title, "TASK REPORT", ha='center', va='center', fontsize=14, fontweight='bold', color=self.COLORS['gold_light'], fontfamily='Arial', style='italic') # Subtitle with date ax.text(5, y_title - 0.4, date_str, ha='center', va='center', fontsize=9, color=self.COLORS['muted'], fontfamily='Microsoft YaHei') # Gold header bar y_header = num_tasks + 1.15 header_bar = Rectangle((0.5, y_header - 0.22), 9.0, 0.44, facecolor=self.COLORS['gold_dark'], edgecolor='none', alpha=0.25) ax.add_patch(header_bar) # Column headers col_x = [0.7, 1.8, 5.2] headers = ['NO.', 'OWNER', 'TASK'] for x, h in zip(col_x, headers): ax.text(x, y_header, h, ha='left', va='center', fontsize=9, fontweight='bold', color=self.COLORS['gold'], fontfamily='Arial') # Task rows for i, task in enumerate(tasks): y = num_tasks - i + 0.45 # Alternating row background with subtle gold tint if i % 2 == 1: row_bg = Rectangle((0.5, y - 0.28), 9.0, 0.56, facecolor=self.COLORS['row_alt'], edgecolor='none', alpha=0.8) ax.add_patch(row_bg) # Row number with gold accent ax.text(col_x[0], y, f"{i + 1:02d}", ha='left', va='center', fontsize=9, color=self.COLORS['gold_dark'], fontweight='bold', fontfamily='Arial') # Executor with status color if task['is_urgent']: name_color = self.COLORS['accent_red'] name_prefix = "! " elif task['is_overdue']: name_color = self.COLORS['accent_orange'] name_prefix = "! " else: name_color = self.COLORS['gold_light'] name_prefix = "" ax.text(col_x[1], y, name_prefix + task['executor'][:4], ha='left', va='center', fontsize=10, fontweight='bold', color=name_color, fontfamily='Microsoft YaHei') # Task title ax.text(col_x[2], y, task['title'], ha='left', va='center', fontsize=9, color=self.COLORS['text'], fontfamily='Microsoft YaHei', alpha=0.9) # Footer with gold accent ax.plot([2, 8], [0.55, 0.55], color=self.COLORS['gold'], linewidth=0.5, alpha=0.4) ax.text(5, 0.35, f"{len(tasks)} TASKS IN PROGRESS", ha='center', va='center', fontsize=8, color=self.COLORS['gold_dark'], fontfamily='Arial', style='italic') plt.tight_layout(pad=0.2) plt.savefig(output_path, dpi=180, facecolor=fig.get_facecolor(), edgecolor='none', bbox_inches='tight', pad_inches=0.08) plt.close(fig) def render_and_upload( summary: Dict, user_roles: Dict[str, str], imgbb_api_key: str, dashboard_url: str = None, switches: Dict[str, bool] = None, output_dir: str = "./data" ) -> Tuple[Optional[str], List[Dict]]: """Render and upload to ImgBB.""" from dingtalk_sender import ImgBBUploader renderer = TableImageRenderer(output_dir) image_path, task_list = renderer.render_task_report(summary, user_roles, dashboard_url, switches) if not image_path: return None, task_list uploader = ImgBBUploader(imgbb_api_key) url = uploader.upload_image(image_path, name=f"report_{datetime.now().strftime('%Y%m%d')}") return url, task_list