This commit is contained in:
MarkLo 2025-12-16 01:42:30 +08:00
parent 4234a181e5
commit dbee0c4817
1 changed files with 198 additions and 71 deletions

View File

@ -752,53 +752,143 @@ class PDFGenerator:
buffer = io.BytesIO() buffer = io.BytesIO()
# Create PDF document # Define team structure
doc = SimpleDocTemplate( TEAMS = [
{
'name': '分析師團隊',
'count': 4,
'members': ['市場分析師', '社群媒體分析師', '新聞分析師', '基本面分析師'],
},
{
'name': '研究團隊',
'count': 3,
'members': ['看漲研究員', '看跌研究員', '研究經理'],
},
{
'name': '交易與風險團隊',
'count': 5,
'members': ['交易員', '激進分析師', '保守分析師', '中立分析師', '風險經理'],
},
]
# Create a mapping of analyst names to their reports
report_map = {r.get('analyst_name', ''): r.get('report_content', '') for r in reports}
# Create PDF document with custom page numbering
# Cover and TOC don't have page numbers
# Page numbering starts from chart page = Page 1
buffer = io.BytesIO()
from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame, NextPageTemplate, PageBreak as PB
from reportlab.lib.pagesizes import A4
from io import BytesIO
# Track pages for numbering (pages after TOC)
self._page_offset = 2 # Cover + TOC = 2 pages without numbers
def add_page_number(canvas, doc):
"""Add page number to footer for content pages"""
page_num = doc.page - self._page_offset
if page_num > 0:
canvas.saveState()
canvas.setFont(self.primary_font, 10)
page_text = f"- {page_num} -"
canvas.drawCentredString(A4[0] / 2, 1 * cm, page_text)
canvas.restoreState()
def no_page_number(canvas, doc):
"""No page number for cover and TOC"""
pass
# Create document
doc = BaseDocTemplate(
buffer, buffer,
pagesize=A4, pagesize=A4,
rightMargin=1.5*cm, rightMargin=1.5*cm,
leftMargin=1.5*cm, leftMargin=1.5*cm,
topMargin=1.5*cm, topMargin=1.5*cm,
bottomMargin=1.5*cm, bottomMargin=2*cm,
) )
# Define frames
frame = Frame(
doc.leftMargin,
doc.bottomMargin,
doc.width,
doc.height,
id='normal'
)
# Define page templates
# 'cover' and 'toc' templates have no page numbers
# 'content' template has page numbers
doc.addPageTemplates([
PageTemplate(id='cover', frames=frame, onPage=no_page_number),
PageTemplate(id='toc', frames=frame, onPage=no_page_number),
PageTemplate(id='content', frames=frame, onPage=add_page_number),
])
elements = [] elements = []
styles = self._get_styles() styles = self._get_styles()
# === COVER PAGE (Page 1) === # === COVER PAGE (no page number) ===
elements.append(NextPageTemplate('cover'))
elements.extend(self._create_cover_page(ticker, analysis_date, styles)) elements.extend(self._create_cover_page(ticker, analysis_date, styles))
elements.append(NextPageTemplate('toc'))
elements.append(PageBreak()) elements.append(PageBreak())
# === TABLE OF CONTENTS PAGE (Page 2) === # Check if we have chart data
# Pass has_chart flag to TOC so it knows to include chart page entry
has_chart = price_data and len(price_data) >= 5 has_chart = price_data and len(price_data) >= 5
elements.extend(self._create_toc_page(ticker, analysis_date, reports, price_data, price_stats, styles, has_chart))
# === TABLE OF CONTENTS PAGE (no page number) ===
elements.extend(self._create_toc_page(ticker, analysis_date, reports, price_data, price_stats, styles, has_chart, TEAMS))
elements.append(NextPageTemplate('content'))
elements.append(PageBreak()) elements.append(PageBreak())
# === PRICE CHART PAGE (Page 3, if data available) === # ========================================
# CONTENT PAGES WITH PAGE NUMBERS
# ========================================
# === PRICE CHART PAGE (Page 1, if data available) ===
if has_chart: if has_chart:
elements.extend(self._create_chart_page(ticker, price_data, price_stats, styles)) elements.extend(self._create_chart_page(ticker, price_data, price_stats, styles))
elements.append(PageBreak()) elements.append(PageBreak())
# === ANALYST REPORTS (Starting from Page 3 or 4) === # === ANALYST REPORTS BY TEAM ===
for i, report in enumerate(reports): for team_idx, team in enumerate(TEAMS):
analyst_name = report.get('analyst_name', 'Unknown') # Get reports for this team
report_content = report.get('report_content', '') team_reports = []
for member_name in team['members']:
if member_name in report_map and report_map[member_name]:
team_reports.append({
'analyst_name': member_name,
'report_content': report_map[member_name]
})
if not report_content: # Skip team if no reports
if not team_reports:
continue continue
# Add analyst report section # Add team separator page
elements.extend(self._create_analyst_section( elements.extend(self._create_team_separator(team['name'], len(team_reports), styles))
analyst_name=analyst_name, elements.append(PageBreak())
ticker=ticker,
analysis_date=analysis_date,
report_content=report_content,
styles=styles,
))
# Page break between analysts (except for the last one) # Add each analyst report in this team
if i < len(reports) - 1: for report_idx, report in enumerate(team_reports):
analyst_name = report.get('analyst_name', 'Unknown')
report_content = report.get('report_content', '')
# Add analyst report section
elements.extend(self._create_analyst_section(
analyst_name=analyst_name,
ticker=ticker,
analysis_date=analysis_date,
report_content=report_content,
styles=styles,
))
# Page break after each analyst
elements.append(PageBreak()) elements.append(PageBreak())
# Build PDF # Build PDF
@ -975,7 +1065,8 @@ class PDFGenerator:
price_data: list, price_data: list,
price_stats: dict, price_stats: dict,
styles: dict, styles: dict,
has_chart: bool = False has_chart: bool = False,
teams: list = None
) -> list: ) -> list:
"""Create a clean table of contents page with page numbers""" """Create a clean table of contents page with page numbers"""
from reportlab.platypus import Spacer, Table, TableStyle from reportlab.platypus import Spacer, Table, TableStyle
@ -987,13 +1078,14 @@ class PDFGenerator:
# TOC Title # TOC Title
elements.append(Paragraph("目 錄", styles['toc_title'])) elements.append(Paragraph("目 錄", styles['toc_title']))
elements.append(Spacer(1, 1*cm)) elements.append(Spacer(1, 0.5*cm))
# Calculate page numbers for each section # Track which analysts are in the reports
# Cover page = 1, TOC = 2 report_analyst_names = [r.get('analyst_name', '') for r in reports]
# If has_chart: Chart page = 3, analysts start from page 4
# If no chart: analysts start from page 3 # Page numbering: Page 1 starts from chart page
current_page = 3 # Cover and TOC don't have page numbers
current_page = 1
# Build TOC table data # Build TOC table data
table_data = [] table_data = []
@ -1005,48 +1097,37 @@ class PDFGenerator:
# Add chart page entry if available # Add chart page entry if available
if has_chart: if has_chart:
table_data.append([ table_data.append([
Paragraph('價格走勢圖 & 交易量柱狀圖', styles['toc_item']), Paragraph(' 價格走勢圖 & 交易量柱狀圖', styles['toc_item']),
Paragraph(f'{current_page}', styles['toc_item']) Paragraph(f'{current_page}', styles['toc_item'])
]) ])
current_page += 1 current_page += 1
# Add separator # Use teams if provided
table_data.append([ if teams:
Paragraph('<b>分析報告</b>', styles['toc_section']), for team in teams:
'' team_name = team['name']
]) team_members = team['members']
# Group analysts by category with page numbers
analyst_categories = [
('分析師組', ['市場分析師', '基本面分析師', '社群媒體分析師', '新聞分析師']),
('研究員組', ['看漲研究員', '看跌研究員']),
('風險辯論組', ['激進分析師', '保守分析師', '中立分析師']),
('決策組', ['研究經理', '風險經理', '交易員']),
]
# Track which analysts are in the reports
report_analyst_names = [r.get('analyst_name', '') for r in reports]
for category, analysts in analyst_categories:
# Check if any analyst in this category exists
category_has_analysts = any(name in report_analyst_names for name in analysts)
if not category_has_analysts:
continue
# Add category header # Count how many members have reports
table_data.append([ team_report_count = sum(1 for m in team_members if m in report_analyst_names)
Paragraph(f' <b>{category}</b>', styles['toc_item']), if team_report_count == 0:
'' continue
])
# Add team separator entry
# Add each analyst table_data.append([
for analyst_name in analysts: Paragraph(f'<b>{team_name} ({team_report_count} 位)</b>', styles['toc_section']),
if analyst_name in report_analyst_names: Paragraph(f'{current_page}', styles['toc_item'])
table_data.append([ ])
Paragraph(f' {analyst_name}', styles['toc_item']), current_page += 1 # Team separator page
Paragraph(f'{current_page}', styles['toc_item'])
]) # Add each analyst in this team
current_page += 1 for analyst_name in team_members:
if analyst_name in report_analyst_names:
table_data.append([
Paragraph(f' {analyst_name}', styles['toc_item']),
Paragraph(f'{current_page}', styles['toc_item'])
])
current_page += 1
# Create table # Create table
col_widths = [14*cm, 2*cm] col_widths = [14*cm, 2*cm]
@ -1059,8 +1140,8 @@ class PDFGenerator:
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('FONTNAME', (0, 0), (-1, -1), self.primary_font), ('FONTNAME', (0, 0), (-1, -1), self.primary_font),
('FONTSIZE', (0, 0), (-1, -1), 11), ('FONTSIZE', (0, 0), (-1, -1), 11),
('BOTTOMPADDING', (0, 0), (-1, -1), 8), ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('TOPPADDING', (0, 0), (-1, -1), 8), ('TOPPADDING', (0, 0), (-1, -1), 6),
('LINEBELOW', (0, 0), (-1, 0), 1, black), # Header line ('LINEBELOW', (0, 0), (-1, 0), 1, black), # Header line
('LINEBELOW', (0, -1), (-1, -1), 0.5, lightgrey), # Bottom line ('LINEBELOW', (0, -1), (-1, -1), 0.5, lightgrey), # Bottom line
]) ])
@ -1070,6 +1151,52 @@ class PDFGenerator:
return elements return elements
def _create_team_separator(
self,
team_name: str,
member_count: int,
styles: dict
) -> list:
"""Create a team separator page"""
from reportlab.platypus import Spacer
from reportlab.lib.units import cm
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.enums import TA_CENTER
from reportlab.lib.colors import HexColor
elements = []
# Center the team name vertically
elements.append(Spacer(1, 8*cm))
# Team name with member count
team_style = ParagraphStyle(
'TeamSeparator',
fontName=self.primary_font,
fontSize=36,
textColor=HexColor('#2c3e50'),
alignment=TA_CENTER,
spaceAfter=20,
leading=50,
)
# Add spaces for better letter spacing
spaced_name = ' '.join(team_name)
elements.append(Paragraph(spaced_name, team_style))
# Member count
count_style = ParagraphStyle(
'TeamCount',
fontName=self.primary_font,
fontSize=24,
textColor=HexColor('#7f8c8d'),
alignment=TA_CENTER,
spaceAfter=10,
)
elements.append(Paragraph(f"({member_count} 位)", count_style))
return elements
def _create_chart_page( def _create_chart_page(
self, self,
ticker: str, ticker: str,
@ -1301,8 +1428,8 @@ class PDFGenerator:
text = line[2:-2] text = line[2:-2]
elements.append(Paragraph(self._escape_html(text), styles['heading'])) elements.append(Paragraph(self._escape_html(text), styles['heading']))
elif line.startswith('- ') or line.startswith('* '): elif line.startswith('- ') or line.startswith('* '):
# Bullet points # Bullet points - use simple dash instead of Unicode bullet
text = ' ' + line[2:] text = ' - ' + line[2:]
elements.append(Paragraph(self._escape_html(text), styles['body'])) elements.append(Paragraph(self._escape_html(text), styles['body']))
else: else:
# Clean any remaining markdown # Clean any remaining markdown