马一丁

Improve SWOT pagination effect

@@ -1178,6 +1178,11 @@ class HTMLRenderer: @@ -1178,6 +1178,11 @@ class HTMLRenderer:
1178 def _render_swot_table(self, block: Dict[str, Any]) -> str: 1178 def _render_swot_table(self, block: Dict[str, Any]) -> str:
1179 """ 1179 """
1180 渲染四象限的SWOT专用表格,兼顾HTML与PDF的可读性。 1180 渲染四象限的SWOT专用表格,兼顾HTML与PDF的可读性。
  1181 +
  1182 + PDF分页策略:
  1183 + - 每个S/W/O/T象限内部禁止分页(break-inside: avoid)
  1184 + - 允许在象限之间分页
  1185 + - 卡片标题与第一个象限尽量保持在一起
1181 """ 1186 """
1182 title = block.get("title") or "SWOT 分析" 1187 title = block.get("title") or "SWOT 分析"
1183 summary = block.get("summary") 1188 summary = block.get("summary")
@@ -1188,12 +1193,14 @@ class HTMLRenderer: @@ -1188,12 +1193,14 @@ class HTMLRenderer:
1188 ("threats", "威胁 Threats", "T", "threat"), 1193 ("threats", "威胁 Threats", "T", "threat"),
1189 ] 1194 ]
1190 cells_html = "" 1195 cells_html = ""
1191 - for key, label, code, css in quadrants: 1196 + for idx, (key, label, code, css) in enumerate(quadrants):
1192 items = self._normalize_swot_items(block.get(key)) 1197 items = self._normalize_swot_items(block.get(key))
1193 caption_text = f"{len(items)} 条要点" if items else "待补充" 1198 caption_text = f"{len(items)} 条要点" if items else "待补充"
1194 list_html = "".join(self._render_swot_item(item) for item in items) if items else '<li class="swot-empty">尚未填入要点</li>' 1199 list_html = "".join(self._render_swot_item(item) for item in items) if items else '<li class="swot-empty">尚未填入要点</li>'
  1200 + # 第一个象限添加特殊类以便与标题保持在一起
  1201 + first_cell_class = " swot-cell--first" if idx == 0 else ""
1195 cells_html += f""" 1202 cells_html += f"""
1196 - <div class="swot-cell {css}"> 1203 + <div class="swot-cell swot-cell--pageable {css}{first_cell_class}" data-swot-key="{key}">
1197 <div class="swot-cell__meta"> 1204 <div class="swot-cell__meta">
1198 <span class="swot-pill {css}">{self._escape_html(code)}</span> 1205 <span class="swot-pill {css}">{self._escape_html(code)}</span>
1199 <div> 1206 <div>
@@ -3208,20 +3215,61 @@ table th {{ @@ -3208,20 +3215,61 @@ table th {{
3208 color: var(--swot-muted); 3215 color: var(--swot-muted);
3209 opacity: 0.7; 3216 opacity: 0.7;
3210 }} 3217 }}
3211 -/* PDF/导出时的SWOT专用布局,避免圆角框重叠 */ 3218 +/* PDF/导出时的SWOT专用布局,支持分页且避免圆角框重叠 */
3212 body.exporting .swot-legend {{ 3219 body.exporting .swot-legend {{
3213 display: none !important; 3220 display: none !important;
3214 }} 3221 }}
3215 body.exporting .swot-grid {{ 3222 body.exporting .swot-grid {{
3216 display: flex; 3223 display: flex;
3217 - flex-wrap: wrap;  
3218 - gap: 12px;  
3219 - align-items: stretch; 3224 + flex-direction: column;
  3225 + gap: 16px;
3220 }} 3226 }}
3221 body.exporting .swot-cell {{ 3227 body.exporting .swot-cell {{
3222 - flex: 1 1 320px;  
3223 - min-width: 240px; 3228 + width: 100%;
3224 height: auto; 3229 height: auto;
  3230 + page-break-inside: avoid;
  3231 + break-inside: avoid;
  3232 +}}
  3233 +body.exporting .swot-cell--first {{
  3234 + page-break-before: avoid;
  3235 + break-before: avoid;
  3236 +}}
  3237 +/* 打印模式下的SWOT分页控制 */
  3238 +@media print {{
  3239 + .swot-card {{
  3240 + break-inside: auto;
  3241 + page-break-inside: auto;
  3242 + }}
  3243 + .swot-card__head {{
  3244 + break-after: avoid;
  3245 + page-break-after: avoid;
  3246 + }}
  3247 + .swot-grid {{
  3248 + display: flex;
  3249 + flex-direction: column;
  3250 + gap: 16px;
  3251 + }}
  3252 + .swot-cell {{
  3253 + break-inside: avoid;
  3254 + page-break-inside: avoid;
  3255 + width: 100%;
  3256 + }}
  3257 + .swot-cell--first {{
  3258 + break-before: avoid;
  3259 + page-break-before: avoid;
  3260 + }}
  3261 + .swot-cell__meta {{
  3262 + break-after: avoid;
  3263 + page-break-after: avoid;
  3264 + }}
  3265 + .swot-list {{
  3266 + break-inside: avoid;
  3267 + page-break-inside: avoid;
  3268 + }}
  3269 + .swot-item {{
  3270 + break-inside: avoid;
  3271 + page-break-inside: avoid;
  3272 + }}
3225 }} 3273 }}
3226 .callout {{ 3274 .callout {{
3227 border-left: 4px solid var(--primary-color); 3275 border-left: 4px solid var(--primary-color);
@@ -1049,35 +1049,78 @@ body {{ @@ -1049,35 +1049,78 @@ body {{
1049 min-height: 400px; 1049 min-height: 400px;
1050 }} 1050 }}
1051 1051
1052 -/* SWOT:PDF导出隐藏四象限标注,并使用自适应布局避免重叠 */ 1052 +/* ========== SWOT PDF分页优化 ========== */
  1053 +/* 核心策略:S/W/O/T四个象限各自内部禁止分页,但允许象限之间分页 */
  1054 +
  1055 +/* 隐藏四象限标注图例 */
1053 .swot-legend {{ 1056 .swot-legend {{
1054 display: none !important; 1057 display: none !important;
1055 }} 1058 }}
  1059 +
  1060 +/* SWOT卡片容器:允许内部分页 */
1056 .swot-card {{ 1061 .swot-card {{
1057 - /* 允许卡片内容在必要时分页,避免整体被抬到下一页 */  
1058 break-inside: auto !important; 1062 break-inside: auto !important;
1059 page-break-inside: auto !important; 1063 page-break-inside: auto !important;
  1064 + margin: 20px 0;
1060 }} 1065 }}
  1066 +
  1067 +/* 卡片头部(标题+摘要):避免紧跟其后分页,尽量与第一个象限保持在一起 */
1061 .swot-card__head {{ 1068 .swot-card__head {{
1062 - break-after: avoid;  
1063 - page-break-after: avoid; 1069 + break-after: avoid !important;
  1070 + page-break-after: avoid !important;
  1071 + break-inside: avoid !important;
  1072 + page-break-inside: avoid !important;
1064 }} 1073 }}
  1074 +
  1075 +/* 网格容器:PDF模式下使用纵向flex布局,允许子元素间分页 */
1065 .swot-grid {{ 1076 .swot-grid {{
1066 - display: flex;  
1067 - flex-wrap: wrap;  
1068 - gap: 12px;  
1069 - align-items: stretch;  
1070 - break-before: avoid;  
1071 - page-break-before: avoid;  
1072 - break-inside: auto;  
1073 - page-break-inside: auto; 1077 + display: flex !important;
  1078 + flex-direction: column !important;
  1079 + gap: 16px !important;
  1080 + break-inside: auto !important;
  1081 + page-break-inside: auto !important;
1074 }} 1082 }}
1075 -.swot-grid .swot-cell {{  
1076 - flex: 1 1 320px;  
1077 - min-width: 240px;  
1078 - height: auto;  
1079 - page-break-inside: avoid;  
1080 - break-inside: avoid; 1083 +
  1084 +/* 每个SWOT象限单元格:禁止内部分页,允许前后分页 */
  1085 +.swot-cell {{
  1086 + break-inside: avoid !important;
  1087 + page-break-inside: avoid !important;
  1088 + break-before: auto;
  1089 + page-break-before: auto;
  1090 + break-after: auto;
  1091 + page-break-after: auto;
  1092 + width: 100% !important;
  1093 + max-width: 100% !important;
  1094 + flex: none !important;
  1095 + min-height: auto !important;
  1096 + height: auto !important;
  1097 + box-sizing: border-box;
  1098 +}}
  1099 +
  1100 +/* 第一个象限:避免在标题后立即分页 */
  1101 +.swot-cell--first {{
  1102 + break-before: avoid !important;
  1103 + page-break-before: avoid !important;
  1104 +}}
  1105 +
  1106 +/* 象限内的meta区域(图标+标题):避免被分页切开 */
  1107 +.swot-cell__meta {{
  1108 + break-inside: avoid !important;
  1109 + page-break-inside: avoid !important;
  1110 + break-after: avoid !important;
  1111 + page-break-after: avoid !important;
  1112 +}}
  1113 +
  1114 +/* 条目列表:允许列表整体分页 */
  1115 +.swot-list {{
  1116 + break-inside: avoid !important;
  1117 + page-break-inside: avoid !important;
  1118 +}}
  1119 +
  1120 +/* 单个条目:避免被分页切开 */
  1121 +.swot-item {{
  1122 + break-inside: avoid !important;
  1123 + page-break-inside: avoid !important;
1081 }} 1124 }}
1082 1125
1083 {optimized_css} 1126 {optimized_css}