马一丁

Modify SWOT analysis

... ... @@ -1177,15 +1177,34 @@ class HTMLRenderer:
def _render_swot_table(self, block: Dict[str, Any]) -> str:
"""
渲染四象限的SWOT专用表格,兼顾HTML与PDF的可读性。
渲染四象限的SWOT分析,同时生成两种布局:
1. 卡片布局(用于HTML网页显示)- 圆角矩形四象限
2. 表格布局(用于PDF导出)- 结构化表格,支持分页
PDF分页策略:
- 每个S/W/O/T象限内部禁止分页(break-inside: avoid)
- 允许在象限之间分页
- 卡片标题与第一个象限尽量保持在一起
- 使用表格形式,每个S/W/O/T象限为独立表格区块
- 允许在不同象限之间分页
- 每个象限内的条目尽量保持在一起
"""
title = block.get("title") or "SWOT 分析"
summary = block.get("summary")
# ========== 卡片布局(HTML用)==========
card_html = self._render_swot_card_layout(block, title, summary)
# ========== 表格布局(PDF用)==========
table_html = self._render_swot_pdf_table_layout(block, title, summary)
# 返回包含两种布局的容器
return f"""
<div class="swot-container">
{card_html}
{table_html}
</div>
"""
def _render_swot_card_layout(self, block: Dict[str, Any], title: str, summary: str | None) -> str:
"""渲染SWOT卡片布局(用于HTML网页显示)"""
quadrants = [
("strengths", "优势 Strengths", "S", "strength"),
("weaknesses", "劣势 Weaknesses", "W", "weakness"),
... ... @@ -1197,7 +1216,6 @@ class HTMLRenderer:
items = self._normalize_swot_items(block.get(key))
caption_text = f"{len(items)} 条要点" if items else "待补充"
list_html = "".join(self._render_swot_item(item) for item in items) if items else '<li class="swot-empty">尚未填入要点</li>'
# 第一个象限添加特殊类以便与标题保持在一起
first_cell_class = " swot-cell--first" if idx == 0 else ""
cells_html += f"""
<div class="swot-cell swot-cell--pageable {css}{first_cell_class}" data-swot-key="{key}">
... ... @@ -1221,7 +1239,7 @@ class HTMLRenderer:
</div>
"""
return f"""
<div class="swot-card">
<div class="swot-card swot-card--html">
<div class="swot-card__head">
<div>{title_html}{summary_html}</div>
{legend}
... ... @@ -1230,6 +1248,121 @@ class HTMLRenderer:
</div>
"""
def _render_swot_pdf_table_layout(self, block: Dict[str, Any], title: str, summary: str | None) -> str:
"""
渲染SWOT表格布局(用于PDF导出)
设计说明:
- 整体为一个大表格,包含标题行和4个象限区域
- 每个象限区域有自己的子标题行和内容行
- 使用合并单元格来显示象限标题
- 通过CSS控制分页行为
"""
quadrants = [
("strengths", "S", "优势 Strengths", "swot-pdf-strength", "#1c7f6e"),
("weaknesses", "W", "劣势 Weaknesses", "swot-pdf-weakness", "#c0392b"),
("opportunities", "O", "机会 Opportunities", "swot-pdf-opportunity", "#1f5ab3"),
("threats", "T", "威胁 Threats", "swot-pdf-threat", "#b36b16"),
]
# 标题和摘要
summary_row = ""
if summary:
summary_row = f"""
<tr class="swot-pdf-summary-row">
<td colspan="4" class="swot-pdf-summary">{self._escape_html(summary)}</td>
</tr>"""
# 生成四个象限的表格内容
quadrant_tables = ""
for idx, (key, code, label, css_class, color) in enumerate(quadrants):
items = self._normalize_swot_items(block.get(key))
# 生成每个象限的内容行
items_rows = ""
if items:
for item_idx, item in enumerate(items):
item_title = item.get("title") or item.get("label") or item.get("text") or "未命名要点"
item_detail = item.get("detail") or item.get("description") or ""
item_evidence = item.get("evidence") or item.get("source") or ""
item_impact = item.get("impact") or item.get("priority") or ""
item_score = item.get("score")
# 构建详情内容
detail_parts = []
if item_detail:
detail_parts.append(item_detail)
if item_evidence:
detail_parts.append(f"佐证:{item_evidence}")
detail_text = "<br/>".join(detail_parts) if detail_parts else "-"
# 构建标签
tags = []
if item_impact:
tags.append(f'<span class="swot-pdf-tag">{self._escape_html(item_impact)}</span>')
if item_score not in (None, ""):
tags.append(f'<span class="swot-pdf-tag swot-pdf-tag--score">评分 {self._escape_html(item_score)}</span>')
tags_html = " ".join(tags)
# 第一行需要合并象限标题单元格
if item_idx == 0:
rowspan = len(items)
items_rows += f"""
<tr class="swot-pdf-item-row {css_class}">
<td rowspan="{rowspan}" class="swot-pdf-quadrant-label {css_class}">
<span class="swot-pdf-code">{code}</span>
<span class="swot-pdf-label-text">{self._escape_html(label.split()[0])}</span>
</td>
<td class="swot-pdf-item-num">{item_idx + 1}</td>
<td class="swot-pdf-item-title">{self._escape_html(item_title)}</td>
<td class="swot-pdf-item-detail">{detail_text}</td>
<td class="swot-pdf-item-tags">{tags_html}</td>
</tr>"""
else:
items_rows += f"""
<tr class="swot-pdf-item-row {css_class}">
<td class="swot-pdf-item-num">{item_idx + 1}</td>
<td class="swot-pdf-item-title">{self._escape_html(item_title)}</td>
<td class="swot-pdf-item-detail">{detail_text}</td>
<td class="swot-pdf-item-tags">{tags_html}</td>
</tr>"""
else:
# 没有内容时显示占位
items_rows = f"""
<tr class="swot-pdf-item-row {css_class}">
<td class="swot-pdf-quadrant-label {css_class}">
<span class="swot-pdf-code">{code}</span>
<span class="swot-pdf-label-text">{self._escape_html(label.split()[0])}</span>
</td>
<td class="swot-pdf-item-num">-</td>
<td colspan="3" class="swot-pdf-empty">暂无要点</td>
</tr>"""
# 每个象限作为一个独立的tbody,便于分页控制
quadrant_tables += f"""
<tbody class="swot-pdf-quadrant {css_class}">
{items_rows}
</tbody>"""
return f"""
<div class="swot-pdf-wrapper">
<table class="swot-pdf-table">
<caption class="swot-pdf-caption">{self._escape_html(title)}</caption>
<thead class="swot-pdf-thead">
<tr>
<th class="swot-pdf-th-quadrant">象限</th>
<th class="swot-pdf-th-num">序号</th>
<th class="swot-pdf-th-title">要点</th>
<th class="swot-pdf-th-detail">详细说明</th>
<th class="swot-pdf-th-tags">影响/评分</th>
</tr>
{summary_row}
</thead>
{quadrant_tables}
</table>
</div>
"""
def _normalize_swot_items(self, raw: Any) -> List[Dict[str, Any]]:
"""将SWOT条目规整为统一结构,兼容字符串/对象两种写法"""
normalized: List[Dict[str, Any]] = []
... ... @@ -3215,26 +3348,121 @@ table th {{
color: var(--swot-muted);
opacity: 0.7;
}}
/* PDF/导出时的SWOT专用布局,支持分页且避免圆角框重叠 */
body.exporting .swot-legend {{
display: none !important;
}}
body.exporting .swot-grid {{
display: flex;
flex-direction: column;
gap: 16px;
/* ========== SWOT PDF表格布局样式(默认隐藏)========== */
.swot-pdf-wrapper {{
display: none;
}}
body.exporting .swot-cell {{
/* SWOT PDF表格样式定义(用于PDF渲染时显示) */
.swot-pdf-table {{
width: 100%;
height: auto;
page-break-inside: avoid;
border-collapse: collapse;
margin: 20px 0;
font-size: 13px;
table-layout: fixed;
}}
.swot-pdf-caption {{
caption-side: top;
text-align: left;
font-size: 1.15rem;
font-weight: 700;
padding: 12px 0;
color: var(--text-color);
}}
.swot-pdf-thead th {{
background: #f8f9fa;
padding: 10px 8px;
text-align: left;
font-weight: 600;
border: 1px solid #dee2e6;
color: #495057;
}}
.swot-pdf-th-quadrant {{ width: 80px; }}
.swot-pdf-th-num {{ width: 50px; text-align: center; }}
.swot-pdf-th-title {{ width: 22%; }}
.swot-pdf-th-detail {{ width: auto; }}
.swot-pdf-th-tags {{ width: 100px; text-align: center; }}
.swot-pdf-summary {{
padding: 12px;
background: #f8f9fa;
color: #666;
font-style: italic;
border: 1px solid #dee2e6;
}}
.swot-pdf-quadrant {{
break-inside: avoid;
page-break-inside: avoid;
}}
body.exporting .swot-cell--first {{
page-break-before: avoid;
break-before: avoid;
.swot-pdf-quadrant-label {{
text-align: center;
vertical-align: middle;
padding: 12px 8px;
font-weight: 700;
border: 1px solid #dee2e6;
writing-mode: horizontal-tb;
}}
.swot-pdf-quadrant-label.swot-pdf-strength {{ background: rgba(28,127,110,0.15); color: #1c7f6e; border-left: 4px solid #1c7f6e; }}
.swot-pdf-quadrant-label.swot-pdf-weakness {{ background: rgba(192,57,43,0.12); color: #c0392b; border-left: 4px solid #c0392b; }}
.swot-pdf-quadrant-label.swot-pdf-opportunity {{ background: rgba(31,90,179,0.12); color: #1f5ab3; border-left: 4px solid #1f5ab3; }}
.swot-pdf-quadrant-label.swot-pdf-threat {{ background: rgba(179,107,22,0.12); color: #b36b16; border-left: 4px solid #b36b16; }}
.swot-pdf-code {{
display: block;
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 4px;
}}
.swot-pdf-label-text {{
display: block;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.02em;
}}
.swot-pdf-item-row td {{
padding: 10px 8px;
border: 1px solid #dee2e6;
vertical-align: top;
}}
.swot-pdf-item-row.swot-pdf-strength td {{ background: rgba(28,127,110,0.03); }}
.swot-pdf-item-row.swot-pdf-weakness td {{ background: rgba(192,57,43,0.03); }}
.swot-pdf-item-row.swot-pdf-opportunity td {{ background: rgba(31,90,179,0.03); }}
.swot-pdf-item-row.swot-pdf-threat td {{ background: rgba(179,107,22,0.03); }}
.swot-pdf-item-num {{
text-align: center;
font-weight: 600;
color: #6c757d;
}}
.swot-pdf-item-title {{
font-weight: 600;
color: #212529;
}}
.swot-pdf-item-detail {{
color: #495057;
line-height: 1.5;
}}
.swot-pdf-item-tags {{
text-align: center;
}}
.swot-pdf-tag {{
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
background: #e9ecef;
color: #495057;
margin: 2px;
}}
/* 打印模式下的SWOT分页控制 */
.swot-pdf-tag--score {{
background: #fff3cd;
color: #856404;
}}
.swot-pdf-empty {{
text-align: center;
color: #adb5bd;
font-style: italic;
}}
/* 打印模式下的SWOT分页控制(保留卡片布局的打印支持) */
@media print {{
.swot-card {{
break-inside: auto;
... ... @@ -3244,29 +3472,7 @@ body.exporting .swot-cell--first {{
break-after: avoid;
page-break-after: avoid;
}}
.swot-grid {{
display: flex;
flex-direction: column;
gap: 16px;
}}
.swot-cell {{
break-inside: avoid;
page-break-inside: avoid;
width: 100%;
}}
.swot-cell--first {{
break-before: avoid;
page-break-before: avoid;
}}
.swot-cell__meta {{
break-after: avoid;
page-break-after: avoid;
}}
.swot-list {{
break-inside: avoid;
page-break-inside: avoid;
}}
.swot-item {{
.swot-pdf-quadrant {{
break-inside: avoid;
page-break-inside: avoid;
}}
... ...
... ... @@ -1049,78 +1049,193 @@ body {{
min-height: 400px;
}}
/* ========== SWOT PDF分页优化 ========== */
/* 核心策略:S/W/O/T四个象限各自内部禁止分页,但允许象限之间分页 */
/* ========== SWOT PDF表格布局 ========== */
/* 核心策略:PDF中使用表格形式而非卡片形式,更适合分页 */
/* 隐藏四象限标注图例 */
.swot-legend {{
/* 隐藏HTML卡片布局,显示PDF表格布局 */
.swot-card--html {{
display: none !important;
}}
/* SWOT卡片容器:允许内部分页 */
.swot-card {{
break-inside: auto !important;
page-break-inside: auto !important;
margin: 20px 0;
.swot-pdf-wrapper {{
display: block !important;
margin: 24px 0;
}}
/* PDF表格整体样式 */
.swot-pdf-table {{
width: 100% !important;
border-collapse: collapse !important;
font-size: 11px !important;
table-layout: fixed !important;
background: white;
}}
/* 卡片头部(标题+摘要):避免紧跟其后分页,尽量与第一个象限保持在一起 */
.swot-card__head {{
/* 表格标题 */
.swot-pdf-caption {{
caption-side: top !important;
text-align: left !important;
font-size: 16px !important;
font-weight: 700 !important;
padding: 12px 0 !important;
color: #1a1a1a !important;
border-bottom: 2px solid #333 !important;
margin-bottom: 8px !important;
}}
/* 表头样式 */
.swot-pdf-thead {{
break-after: avoid !important;
page-break-after: avoid !important;
break-inside: avoid !important;
page-break-inside: avoid !important;
}}
/* 网格容器:PDF模式下使用纵向flex布局,允许子元素间分页 */
.swot-grid {{
display: flex !important;
flex-direction: column !important;
gap: 16px !important;
break-inside: auto !important;
page-break-inside: auto !important;
.swot-pdf-thead th {{
background: #f0f0f0 !important;
padding: 10px 8px !important;
text-align: left !important;
font-weight: 600 !important;
border: 1px solid #ccc !important;
color: #333 !important;
font-size: 11px !important;
}}
.swot-pdf-th-quadrant {{ width: 70px !important; }}
.swot-pdf-th-num {{ width: 40px !important; text-align: center !important; }}
.swot-pdf-th-title {{ width: 20% !important; }}
.swot-pdf-th-detail {{ width: auto !important; }}
.swot-pdf-th-tags {{ width: 80px !important; text-align: center !important; }}
/* 摘要行 */
.swot-pdf-summary {{
padding: 10px 12px !important;
background: #f8f8f8 !important;
color: #555 !important;
font-style: italic !important;
border: 1px solid #ccc !important;
font-size: 11px !important;
}}
/* 每个SWOT象限单元格:禁止内部分页,允许前后分页 */
.swot-cell {{
/* 每个象限区块 - 核心分页控制 */
.swot-pdf-quadrant {{
break-inside: avoid !important;
page-break-inside: avoid !important;
}}
/* 允许在不同象限之间分页 */
.swot-pdf-quadrant + .swot-pdf-quadrant {{
break-before: auto;
page-break-before: auto;
break-after: auto;
page-break-after: auto;
width: 100% !important;
max-width: 100% !important;
flex: none !important;
min-height: auto !important;
height: auto !important;
box-sizing: border-box;
}}
/* 第一个象限:避免在标题后立即分页 */
.swot-cell--first {{
break-before: avoid !important;
page-break-before: avoid !important;
/* 象限标签单元格 */
.swot-pdf-quadrant-label {{
text-align: center !important;
vertical-align: middle !important;
padding: 12px 6px !important;
font-weight: 700 !important;
border: 1px solid #ccc !important;
width: 70px !important;
}}
/* 象限内的meta区域(图标+标题):避免被分页切开 */
.swot-cell__meta {{
break-inside: avoid !important;
page-break-inside: avoid !important;
break-after: avoid !important;
page-break-after: avoid !important;
/* 四个象限的颜色主题 */
.swot-pdf-quadrant-label.swot-pdf-strength {{
background: #e8f5f2 !important;
color: #1c7f6e !important;
border-left: 4px solid #1c7f6e !important;
}}
.swot-pdf-quadrant-label.swot-pdf-weakness {{
background: #fdeaea !important;
color: #c0392b !important;
border-left: 4px solid #c0392b !important;
}}
.swot-pdf-quadrant-label.swot-pdf-opportunity {{
background: #e8f0fa !important;
color: #1f5ab3 !important;
border-left: 4px solid #1f5ab3 !important;
}}
.swot-pdf-quadrant-label.swot-pdf-threat {{
background: #fdf3e6 !important;
color: #b36b16 !important;
border-left: 4px solid #b36b16 !important;
}}
/* 条目列表:允许列表整体分页 */
.swot-list {{
break-inside: avoid !important;
page-break-inside: avoid !important;
/* 象限代码字母 */
.swot-pdf-code {{
display: block !important;
font-size: 20px !important;
font-weight: 800 !important;
margin-bottom: 2px !important;
}}
/* 单个条目:避免被分页切开 */
.swot-item {{
break-inside: avoid !important;
page-break-inside: avoid !important;
/* 象限标签文字 */
.swot-pdf-label-text {{
display: block !important;
font-size: 9px !important;
font-weight: 600 !important;
letter-spacing: 0.02em !important;
}}
/* 数据行 */
.swot-pdf-item-row td {{
padding: 8px 6px !important;
border: 1px solid #ddd !important;
vertical-align: top !important;
font-size: 11px !important;
line-height: 1.4 !important;
}}
/* 行背景色 */
.swot-pdf-item-row.swot-pdf-strength td {{ background: #f7fbfa !important; }}
.swot-pdf-item-row.swot-pdf-weakness td {{ background: #fef9f9 !important; }}
.swot-pdf-item-row.swot-pdf-opportunity td {{ background: #f7f9fc !important; }}
.swot-pdf-item-row.swot-pdf-threat td {{ background: #fdfbf7 !important; }}
/* 序号单元格 */
.swot-pdf-item-num {{
text-align: center !important;
font-weight: 600 !important;
color: #888 !important;
width: 40px !important;
}}
/* 要点标题 */
.swot-pdf-item-title {{
font-weight: 600 !important;
color: #222 !important;
}}
/* 详情说明 */
.swot-pdf-item-detail {{
color: #444 !important;
line-height: 1.5 !important;
}}
/* 标签单元格 */
.swot-pdf-item-tags {{
text-align: center !important;
}}
/* 标签样式 */
.swot-pdf-tag {{
display: inline-block !important;
padding: 2px 6px !important;
border-radius: 3px !important;
font-size: 9px !important;
background: #e9ecef !important;
color: #495057 !important;
margin: 1px !important;
}}
.swot-pdf-tag--score {{
background: #fff3cd !important;
color: #856404 !important;
}}
/* 空数据提示 */
.swot-pdf-empty {{
text-align: center !important;
color: #999 !important;
font-style: italic !important;
}}
{optimized_css}
... ...