feat:飞书文档补全
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user