Showing
1 changed file
with
360 additions
and
38 deletions
| @@ -8,13 +8,16 @@ PDF布局优化器 | @@ -8,13 +8,16 @@ PDF布局优化器 | ||
| 8 | - 调整色块大小 | 8 | - 调整色块大小 |
| 9 | - 智能排列信息块 | 9 | - 智能排列信息块 |
| 10 | - 保存和加载优化方案 | 10 | - 保存和加载优化方案 |
| 11 | +- 文本宽度检测和溢出预防 | ||
| 12 | +- 色块边界检测和自动调整 | ||
| 11 | """ | 13 | """ |
| 12 | 14 | ||
| 13 | from __future__ import annotations | 15 | from __future__ import annotations |
| 14 | 16 | ||
| 15 | import json | 17 | import json |
| 18 | +import re | ||
| 16 | from pathlib import Path | 19 | from pathlib import Path |
| 17 | -from typing import Any, Dict, List, Optional | 20 | +from typing import Any, Dict, List, Optional, Tuple |
| 18 | from dataclasses import dataclass, asdict | 21 | from dataclasses import dataclass, asdict |
| 19 | from datetime import datetime | 22 | from datetime import datetime |
| 20 | from loguru import logger | 23 | from loguru import logger |
| @@ -139,6 +142,16 @@ class PDFLayoutOptimizer: | @@ -139,6 +142,16 @@ class PDFLayoutOptimizer: | ||
| 139 | 根据内容特征自动优化PDF布局,防止溢出和排版问题。 | 142 | 根据内容特征自动优化PDF布局,防止溢出和排版问题。 |
| 140 | """ | 143 | """ |
| 141 | 144 | ||
| 145 | + # 字符宽度估算系数(基于常见中文字体) | ||
| 146 | + # 中文字符通常是等宽的,约等于字号的像素值 | ||
| 147 | + # 英文和数字约为字号的0.5-0.6倍 | ||
| 148 | + CHAR_WIDTH_FACTOR = { | ||
| 149 | + 'chinese': 1.0, # 中文字符 | ||
| 150 | + 'english': 0.55, # 英文字母 | ||
| 151 | + 'number': 0.6, # 数字 | ||
| 152 | + 'symbol': 0.4, # 符号 | ||
| 153 | + } | ||
| 154 | + | ||
| 142 | def __init__(self, config: Optional[PDFLayoutConfig] = None): | 155 | def __init__(self, config: Optional[PDFLayoutConfig] = None): |
| 143 | """ | 156 | """ |
| 144 | 初始化优化器 | 157 | 初始化优化器 |
| @@ -208,23 +221,40 @@ class PDFLayoutOptimizer: | @@ -208,23 +221,40 @@ class PDFLayoutOptimizer: | ||
| 208 | 'has_long_text': False, | 221 | 'has_long_text': False, |
| 209 | } | 222 | } |
| 210 | 223 | ||
| 224 | + # 优先使用chapters,fallback到sections | ||
| 225 | + chapters = document_ir.get('chapters', []) | ||
| 226 | + if not chapters: | ||
| 227 | + chapters = document_ir.get('sections', []) | ||
| 228 | + | ||
| 211 | # 遍历章节 | 229 | # 遍历章节 |
| 212 | - sections = document_ir.get('sections', []) | ||
| 213 | - for section in sections: | ||
| 214 | - self._analyze_section(section, stats) | 230 | + for chapter in chapters: |
| 231 | + self._analyze_chapter(chapter, stats) | ||
| 215 | 232 | ||
| 216 | logger.info(f"文档分析完成: {stats}") | 233 | logger.info(f"文档分析完成: {stats}") |
| 217 | return stats | 234 | return stats |
| 218 | 235 | ||
| 219 | - def _analyze_section(self, section: Dict[str, Any], stats: Dict[str, Any]): | ||
| 220 | - """递归分析章节""" | ||
| 221 | - children = section.get('children', []) | 236 | + def _analyze_chapter(self, chapter: Dict[str, Any], stats: Dict[str, Any]): |
| 237 | + """分析单个章节""" | ||
| 238 | + # 分析章节中的blocks | ||
| 239 | + blocks = chapter.get('blocks', []) | ||
| 240 | + for block in blocks: | ||
| 241 | + self._analyze_block(block, stats) | ||
| 222 | 242 | ||
| 243 | + # 递归处理子章节(如果有) | ||
| 244 | + children = chapter.get('children', []) | ||
| 223 | for child in children: | 245 | for child in children: |
| 224 | - node_type = child.get('type') | 246 | + if isinstance(child, dict): |
| 247 | + self._analyze_chapter(child, stats) | ||
| 248 | + | ||
| 249 | + def _analyze_block(self, block: Dict[str, Any], stats: Dict[str, Any]): | ||
| 250 | + """分析单个block节点""" | ||
| 251 | + if not isinstance(block, dict): | ||
| 252 | + return | ||
| 253 | + | ||
| 254 | + node_type = block.get('type') | ||
| 225 | 255 | ||
| 226 | - if node_type == 'kpi_grid': | ||
| 227 | - kpis = child.get('kpis', []) | 256 | + if node_type == 'kpiGrid': |
| 257 | + kpis = block.get('items', []) | ||
| 228 | stats['kpi_count'] += len(kpis) | 258 | stats['kpi_count'] += len(kpis) |
| 229 | 259 | ||
| 230 | # 检查KPI数值长度 | 260 | # 检查KPI数值长度 |
| @@ -239,8 +269,16 @@ class PDFLayoutOptimizer: | @@ -239,8 +269,16 @@ class PDFLayoutOptimizer: | ||
| 239 | stats['table_count'] += 1 | 269 | stats['table_count'] += 1 |
| 240 | 270 | ||
| 241 | # 分析表格结构 | 271 | # 分析表格结构 |
| 242 | - headers = child.get('headers', []) | ||
| 243 | - rows = child.get('rows', []) | 272 | + headers = block.get('headers', []) |
| 273 | + rows = block.get('rows', []) | ||
| 274 | + if rows and isinstance(rows[0], dict): | ||
| 275 | + # 从第一行的cells计算列数 | ||
| 276 | + cells = rows[0].get('cells', []) | ||
| 277 | + stats['max_table_columns'] = max( | ||
| 278 | + stats['max_table_columns'], | ||
| 279 | + len(cells) | ||
| 280 | + ) | ||
| 281 | + else: | ||
| 244 | stats['max_table_columns'] = max( | 282 | stats['max_table_columns'] = max( |
| 245 | stats['max_table_columns'], | 283 | stats['max_table_columns'], |
| 246 | len(headers) | 284 | len(headers) |
| @@ -250,24 +288,153 @@ class PDFLayoutOptimizer: | @@ -250,24 +288,153 @@ class PDFLayoutOptimizer: | ||
| 250 | len(rows) | 288 | len(rows) |
| 251 | ) | 289 | ) |
| 252 | 290 | ||
| 253 | - elif node_type == 'chart': | 291 | + elif node_type == 'chart' or node_type == 'widget': |
| 254 | stats['chart_count'] += 1 | 292 | stats['chart_count'] += 1 |
| 255 | 293 | ||
| 256 | elif node_type == 'callout': | 294 | elif node_type == 'callout': |
| 257 | stats['callout_count'] += 1 | 295 | stats['callout_count'] += 1 |
| 258 | - content = child.get('content', '') | ||
| 259 | - if len(content) > 200: | 296 | + # 检查callout中的blocks |
| 297 | + callout_blocks = block.get('blocks', []) | ||
| 298 | + for cb in callout_blocks: | ||
| 299 | + if isinstance(cb, dict) and cb.get('type') == 'paragraph': | ||
| 300 | + text = self._extract_text_from_paragraph(cb) | ||
| 301 | + if len(text) > 200: | ||
| 260 | stats['has_long_text'] = True | 302 | stats['has_long_text'] = True |
| 261 | 303 | ||
| 262 | elif node_type == 'paragraph': | 304 | elif node_type == 'paragraph': |
| 263 | - text = child.get('text', '') | 305 | + text = self._extract_text_from_paragraph(block) |
| 264 | stats['total_content_length'] += len(text) | 306 | stats['total_content_length'] += len(text) |
| 265 | if len(text) > 500: | 307 | if len(text) > 500: |
| 266 | stats['has_long_text'] = True | 308 | stats['has_long_text'] = True |
| 267 | 309 | ||
| 268 | - # 递归处理子章节 | ||
| 269 | - if node_type == 'section': | ||
| 270 | - self._analyze_section(child, stats) | 310 | + # 递归处理嵌套的blocks |
| 311 | + nested_blocks = block.get('blocks', []) | ||
| 312 | + if nested_blocks: | ||
| 313 | + for nested in nested_blocks: | ||
| 314 | + self._analyze_block(nested, stats) | ||
| 315 | + | ||
| 316 | + def _extract_text_from_paragraph(self, paragraph: Dict[str, Any]) -> str: | ||
| 317 | + """从paragraph block中提取纯文本""" | ||
| 318 | + text_parts = [] | ||
| 319 | + inlines = paragraph.get('inlines', []) | ||
| 320 | + for inline in inlines: | ||
| 321 | + if isinstance(inline, dict): | ||
| 322 | + text = inline.get('text', '') | ||
| 323 | + if text: | ||
| 324 | + text_parts.append(str(text)) | ||
| 325 | + elif isinstance(inline, str): | ||
| 326 | + text_parts.append(inline) | ||
| 327 | + return ''.join(text_parts) | ||
| 328 | + | ||
| 329 | + def _analyze_section(self, section: Dict[str, Any], stats: Dict[str, Any]): | ||
| 330 | + """递归分析章节(保留用于向后兼容)""" | ||
| 331 | + # 这个方法保留用于向后兼容,实际上调用_analyze_chapter | ||
| 332 | + self._analyze_chapter(section, stats) | ||
| 333 | + | ||
| 334 | + def _estimate_text_width(self, text: str, font_size: int) -> float: | ||
| 335 | + """ | ||
| 336 | + 估算文本的像素宽度 | ||
| 337 | + | ||
| 338 | + 参数: | ||
| 339 | + text: 要测量的文本 | ||
| 340 | + font_size: 字号(像素) | ||
| 341 | + | ||
| 342 | + 返回: | ||
| 343 | + float: 估算的宽度(像素) | ||
| 344 | + """ | ||
| 345 | + if not text: | ||
| 346 | + return 0.0 | ||
| 347 | + | ||
| 348 | + width = 0.0 | ||
| 349 | + for char in text: | ||
| 350 | + if '\u4e00' <= char <= '\u9fff': # 中文字符范围 | ||
| 351 | + width += font_size * self.CHAR_WIDTH_FACTOR['chinese'] | ||
| 352 | + elif char.isalpha(): | ||
| 353 | + width += font_size * self.CHAR_WIDTH_FACTOR['english'] | ||
| 354 | + elif char.isdigit(): | ||
| 355 | + width += font_size * self.CHAR_WIDTH_FACTOR['number'] | ||
| 356 | + else: | ||
| 357 | + width += font_size * self.CHAR_WIDTH_FACTOR['symbol'] | ||
| 358 | + | ||
| 359 | + return width | ||
| 360 | + | ||
| 361 | + def _check_text_overflow(self, text: str, font_size: int, max_width: int) -> bool: | ||
| 362 | + """ | ||
| 363 | + 检查文本是否会溢出 | ||
| 364 | + | ||
| 365 | + 参数: | ||
| 366 | + text: 要检查的文本 | ||
| 367 | + font_size: 字号(像素) | ||
| 368 | + max_width: 最大宽度(像素) | ||
| 369 | + | ||
| 370 | + 返回: | ||
| 371 | + bool: True表示会溢出 | ||
| 372 | + """ | ||
| 373 | + estimated_width = self._estimate_text_width(text, font_size) | ||
| 374 | + return estimated_width > max_width | ||
| 375 | + | ||
| 376 | + def _calculate_safe_font_size( | ||
| 377 | + self, | ||
| 378 | + text: str, | ||
| 379 | + max_width: int, | ||
| 380 | + min_font_size: int = 10, | ||
| 381 | + max_font_size: int = 32 | ||
| 382 | + ) -> Tuple[int, bool]: | ||
| 383 | + """ | ||
| 384 | + 计算安全的字号以避免溢出 | ||
| 385 | + | ||
| 386 | + 参数: | ||
| 387 | + text: 要显示的文本 | ||
| 388 | + max_width: 最大宽度(像素) | ||
| 389 | + min_font_size: 最小字号 | ||
| 390 | + max_font_size: 最大字号 | ||
| 391 | + | ||
| 392 | + 返回: | ||
| 393 | + Tuple[int, bool]: (建议字号, 是否需要调整) | ||
| 394 | + """ | ||
| 395 | + if not text: | ||
| 396 | + return max_font_size, False | ||
| 397 | + | ||
| 398 | + # 从最大字号开始尝试 | ||
| 399 | + for font_size in range(max_font_size, min_font_size - 1, -1): | ||
| 400 | + if not self._check_text_overflow(text, font_size, max_width): | ||
| 401 | + # 如果需要缩小字号 | ||
| 402 | + needs_adjustment = font_size < max_font_size | ||
| 403 | + return font_size, needs_adjustment | ||
| 404 | + | ||
| 405 | + # 如果连最小字号都溢出,返回最小字号并标记需要调整 | ||
| 406 | + return min_font_size, True | ||
| 407 | + | ||
| 408 | + def _detect_kpi_overflow_issues(self, stats: Dict[str, Any]) -> List[str]: | ||
| 409 | + """ | ||
| 410 | + 检测KPI卡片可能的溢出问题 | ||
| 411 | + | ||
| 412 | + 参数: | ||
| 413 | + stats: 文档统计信息 | ||
| 414 | + | ||
| 415 | + 返回: | ||
| 416 | + List[str]: 检测到的问题列表 | ||
| 417 | + """ | ||
| 418 | + issues = [] | ||
| 419 | + | ||
| 420 | + # KPI卡片的典型宽度(像素) | ||
| 421 | + # 基于2列布局,容器宽度800px,间距20px | ||
| 422 | + kpi_card_width = (800 - 20) // 2 - 40 # 减去padding | ||
| 423 | + | ||
| 424 | + # 检查最长KPI数值 | ||
| 425 | + max_kpi_length = stats.get('max_kpi_value_length', 0) | ||
| 426 | + if max_kpi_length > 0: | ||
| 427 | + # 假设一个很长的数值 | ||
| 428 | + sample_text = '1' * max_kpi_length + '亿元' | ||
| 429 | + current_font_size = self.config.kpi_card.font_size_value | ||
| 430 | + | ||
| 431 | + if self._check_text_overflow(sample_text, current_font_size, kpi_card_width): | ||
| 432 | + issues.append( | ||
| 433 | + f"KPI数值过长({max_kpi_length}字符)," | ||
| 434 | + f"字号{current_font_size}px可能导致溢出" | ||
| 435 | + ) | ||
| 436 | + | ||
| 437 | + return issues | ||
| 271 | 438 | ||
| 272 | def _adjust_config_based_on_stats( | 439 | def _adjust_config_based_on_stats( |
| 273 | self, | 440 | self, |
| @@ -287,37 +454,73 @@ class PDFLayoutOptimizer: | @@ -287,37 +454,73 @@ class PDFLayoutOptimizer: | ||
| 287 | optimize_for_print=self.config.optimize_for_print, | 454 | optimize_for_print=self.config.optimize_for_print, |
| 288 | ) | 455 | ) |
| 289 | 456 | ||
| 290 | - # 根据KPI数值长度调整字号 | ||
| 291 | - if stats['max_kpi_value_length'] > 10: | ||
| 292 | - config.kpi_card.font_size_value = 28 | 457 | + # 检测KPI溢出问题 |
| 458 | + overflow_issues = self._detect_kpi_overflow_issues(stats) | ||
| 459 | + if overflow_issues: | ||
| 460 | + for issue in overflow_issues: | ||
| 461 | + logger.warning(f"检测到布局问题: {issue}") | ||
| 462 | + | ||
| 463 | + # KPI卡片宽度(像素) | ||
| 464 | + kpi_card_width = (800 - 20) // 2 - 40 # 2列布局 | ||
| 465 | + | ||
| 466 | + # 根据KPI数值长度智能调整字号 | ||
| 467 | + if stats['max_kpi_value_length'] > 0: | ||
| 468 | + # 创建示例文本进行测试 | ||
| 469 | + sample_text = '9' * stats['max_kpi_value_length'] | ||
| 470 | + safe_font_size, needs_adjustment = self._calculate_safe_font_size( | ||
| 471 | + sample_text, | ||
| 472 | + kpi_card_width, | ||
| 473 | + min_font_size=18, | ||
| 474 | + max_font_size=32 | ||
| 475 | + ) | ||
| 476 | + | ||
| 477 | + if needs_adjustment: | ||
| 478 | + config.kpi_card.font_size_value = safe_font_size | ||
| 293 | self.optimization_log.append( | 479 | self.optimization_log.append( |
| 294 | f"KPI数值过长({stats['max_kpi_value_length']}字符)," | 480 | f"KPI数值过长({stats['max_kpi_value_length']}字符)," |
| 295 | - f"字号从32调整为28" | 481 | + f"字号自动调整为{safe_font_size}px以防止溢出" |
| 296 | ) | 482 | ) |
| 297 | - elif stats['max_kpi_value_length'] > 15: | ||
| 298 | - config.kpi_card.font_size_value = 24 | 483 | + elif stats['max_kpi_value_length'] > 10: |
| 484 | + # 即使不溢出,也适当缩小以留出更多空间 | ||
| 485 | + config.kpi_card.font_size_value = min(28, safe_font_size) | ||
| 299 | self.optimization_log.append( | 486 | self.optimization_log.append( |
| 300 | - f"KPI数值很长({stats['max_kpi_value_length']}字符)," | ||
| 301 | - f"字号从32调整为24" | 487 | + f"KPI数值较长({stats['max_kpi_value_length']}字符)," |
| 488 | + f"预防性调整字号为{config.kpi_card.font_size_value}px" | ||
| 302 | ) | 489 | ) |
| 303 | 490 | ||
| 304 | - # 根据KPI数量调整网格列数 | 491 | + # 根据KPI数量调整网格布局 |
| 305 | if stats['kpi_count'] > 6: | 492 | if stats['kpi_count'] > 6: |
| 306 | config.grid.columns = 3 | 493 | config.grid.columns = 3 |
| 307 | config.kpi_card.min_height = 100 | 494 | config.kpi_card.min_height = 100 |
| 495 | + config.kpi_card.padding = 16 # 缩小padding以节省空间 | ||
| 308 | self.optimization_log.append( | 496 | self.optimization_log.append( |
| 309 | f"KPI卡片较多({stats['kpi_count']}个)," | 497 | f"KPI卡片较多({stats['kpi_count']}个)," |
| 310 | - f"每行列数从2调整为3" | 498 | + f"调整为3列布局并缩小内边距" |
| 499 | + ) | ||
| 500 | + elif stats['kpi_count'] > 4: | ||
| 501 | + config.grid.columns = 2 | ||
| 502 | + config.kpi_card.padding = 18 | ||
| 503 | + self.optimization_log.append( | ||
| 504 | + f"KPI卡片适中({stats['kpi_count']}个),使用2列布局" | ||
| 311 | ) | 505 | ) |
| 312 | elif stats['kpi_count'] <= 2: | 506 | elif stats['kpi_count'] <= 2: |
| 313 | config.grid.columns = 1 | 507 | config.grid.columns = 1 |
| 508 | + config.kpi_card.padding = 24 # 较少卡片时增加padding | ||
| 314 | self.optimization_log.append( | 509 | self.optimization_log.append( |
| 315 | f"KPI卡片较少({stats['kpi_count']}个)," | 510 | f"KPI卡片较少({stats['kpi_count']}个)," |
| 316 | - f"每行列数从2调整为1" | 511 | + f"使用1列布局并增加内边距" |
| 317 | ) | 512 | ) |
| 318 | 513 | ||
| 319 | - # 根据表格列数调整字号 | ||
| 320 | - if stats['max_table_columns'] > 6: | 514 | + # 根据表格列数调整字号和间距 |
| 515 | + if stats['max_table_columns'] > 8: | ||
| 516 | + config.table.font_size_header = 10 | ||
| 517 | + config.table.font_size_body = 9 | ||
| 518 | + config.table.cell_padding = 6 | ||
| 519 | + self.optimization_log.append( | ||
| 520 | + f"表格列数很多({stats['max_table_columns']}列)," | ||
| 521 | + f"大幅缩小字号和内边距" | ||
| 522 | + ) | ||
| 523 | + elif stats['max_table_columns'] > 6: | ||
| 321 | config.table.font_size_header = 11 | 524 | config.table.font_size_header = 11 |
| 322 | config.table.font_size_body = 10 | 525 | config.table.font_size_body = 10 |
| 323 | config.table.cell_padding = 8 | 526 | config.table.cell_padding = 8 |
| @@ -325,13 +528,34 @@ class PDFLayoutOptimizer: | @@ -325,13 +528,34 @@ class PDFLayoutOptimizer: | ||
| 325 | f"表格列数较多({stats['max_table_columns']}列)," | 528 | f"表格列数较多({stats['max_table_columns']}列)," |
| 326 | f"缩小字号和内边距" | 529 | f"缩小字号和内边距" |
| 327 | ) | 530 | ) |
| 531 | + elif stats['max_table_columns'] > 4: | ||
| 532 | + config.table.font_size_header = 12 | ||
| 533 | + config.table.font_size_body = 11 | ||
| 534 | + config.table.cell_padding = 10 | ||
| 535 | + self.optimization_log.append( | ||
| 536 | + f"表格列数适中({stats['max_table_columns']}列)," | ||
| 537 | + f"适度调整字号" | ||
| 538 | + ) | ||
| 328 | 539 | ||
| 329 | - # 如果有长文本,增加行高 | 540 | + # 如果有长文本,增加行高和段落间距 |
| 330 | if stats['has_long_text']: | 541 | if stats['has_long_text']: |
| 331 | config.page.line_height = 1.8 | 542 | config.page.line_height = 1.8 |
| 332 | config.callout.line_height = 1.8 | 543 | config.callout.line_height = 1.8 |
| 544 | + config.page.paragraph_spacing = 18 | ||
| 333 | self.optimization_log.append( | 545 | self.optimization_log.append( |
| 334 | - "检测到长文本,增加行高至1.8提高可读性" | 546 | + "检测到长文本,增加行高至1.8和段落间距以提高可读性" |
| 547 | + ) | ||
| 548 | + | ||
| 549 | + # 如果内容较多,减小整体字号 | ||
| 550 | + total_blocks = (stats['kpi_count'] + stats['table_count'] + | ||
| 551 | + stats['chart_count'] + stats['callout_count']) | ||
| 552 | + if total_blocks > 20: | ||
| 553 | + config.page.font_size_base = 13 | ||
| 554 | + config.page.font_size_h2 = 22 | ||
| 555 | + config.page.font_size_h3 = 18 | ||
| 556 | + self.optimization_log.append( | ||
| 557 | + f"内容块较多({total_blocks}个)," | ||
| 558 | + f"适度缩小整体字号以优化排版" | ||
| 335 | ) | 559 | ) |
| 336 | 560 | ||
| 337 | return config | 561 | return config |
| @@ -446,7 +670,7 @@ p {{ | @@ -446,7 +670,7 @@ p {{ | ||
| 446 | margin-bottom: {cfg.page.section_spacing}px; | 670 | margin-bottom: {cfg.page.section_spacing}px; |
| 447 | }} | 671 | }} |
| 448 | 672 | ||
| 449 | -/* KPI卡片优化 */ | 673 | +/* KPI卡片优化 - 防止溢出 */ |
| 450 | .kpi-grid {{ | 674 | .kpi-grid {{ |
| 451 | display: grid; | 675 | display: grid; |
| 452 | grid-template-columns: repeat({cfg.grid.columns}, 1fr); | 676 | grid-template-columns: repeat({cfg.grid.columns}, 1fr); |
| @@ -459,58 +683,93 @@ p {{ | @@ -459,58 +683,93 @@ p {{ | ||
| 459 | min-height: {cfg.kpi_card.min_height}px; | 683 | min-height: {cfg.kpi_card.min_height}px; |
| 460 | break-inside: avoid; | 684 | break-inside: avoid; |
| 461 | page-break-inside: avoid; | 685 | page-break-inside: avoid; |
| 686 | + /* 防止溢出的关键设置 */ | ||
| 687 | + overflow: hidden; | ||
| 688 | + box-sizing: border-box; | ||
| 689 | + max-width: 100%; | ||
| 462 | }} | 690 | }} |
| 463 | 691 | ||
| 464 | .kpi-card .value {{ | 692 | .kpi-card .value {{ |
| 465 | font-size: {cfg.kpi_card.font_size_value}px !important; | 693 | font-size: {cfg.kpi_card.font_size_value}px !important; |
| 466 | line-height: 1.2; | 694 | line-height: 1.2; |
| 695 | + /* 强制换行和溢出控制 */ | ||
| 467 | word-break: break-word; | 696 | word-break: break-word; |
| 697 | + overflow-wrap: break-word; | ||
| 698 | + hyphens: auto; | ||
| 699 | + max-width: 100%; | ||
| 700 | + overflow: hidden; | ||
| 701 | + text-overflow: ellipsis; | ||
| 468 | }} | 702 | }} |
| 469 | 703 | ||
| 470 | .kpi-card .label {{ | 704 | .kpi-card .label {{ |
| 471 | font-size: {cfg.kpi_card.font_size_label}px !important; | 705 | font-size: {cfg.kpi_card.font_size_label}px !important; |
| 706 | + /* 防止标签溢出 */ | ||
| 707 | + word-break: break-word; | ||
| 708 | + overflow-wrap: break-word; | ||
| 709 | + max-width: 100%; | ||
| 472 | }} | 710 | }} |
| 473 | 711 | ||
| 474 | .kpi-card .change {{ | 712 | .kpi-card .change {{ |
| 475 | font-size: {cfg.kpi_card.font_size_change}px !important; | 713 | font-size: {cfg.kpi_card.font_size_change}px !important; |
| 714 | + word-break: break-word; | ||
| 476 | }} | 715 | }} |
| 477 | 716 | ||
| 478 | -/* 提示框优化 */ | 717 | +/* 提示框优化 - 防止溢出 */ |
| 479 | .callout {{ | 718 | .callout {{ |
| 480 | padding: {cfg.callout.padding}px !important; | 719 | padding: {cfg.callout.padding}px !important; |
| 481 | margin: 20px 0; | 720 | margin: 20px 0; |
| 482 | line-height: {cfg.callout.line_height}; | 721 | line-height: {cfg.callout.line_height}; |
| 483 | break-inside: avoid; | 722 | break-inside: avoid; |
| 484 | page-break-inside: avoid; | 723 | page-break-inside: avoid; |
| 724 | + /* 防止溢出 */ | ||
| 725 | + overflow: hidden; | ||
| 726 | + box-sizing: border-box; | ||
| 727 | + max-width: 100%; | ||
| 485 | }} | 728 | }} |
| 486 | 729 | ||
| 487 | .callout-title {{ | 730 | .callout-title {{ |
| 488 | font-size: {cfg.callout.font_size_title}px !important; | 731 | font-size: {cfg.callout.font_size_title}px !important; |
| 489 | margin-bottom: 10px; | 732 | margin-bottom: 10px; |
| 733 | + word-break: break-word; | ||
| 490 | }} | 734 | }} |
| 491 | 735 | ||
| 492 | .callout-content {{ | 736 | .callout-content {{ |
| 493 | font-size: {cfg.callout.font_size_content}px !important; | 737 | font-size: {cfg.callout.font_size_content}px !important; |
| 738 | + word-break: break-word; | ||
| 739 | + overflow-wrap: break-word; | ||
| 494 | }} | 740 | }} |
| 495 | 741 | ||
| 496 | -/* 表格优化 */ | 742 | +/* 表格优化 - 严格防止溢出 */ |
| 497 | table {{ | 743 | table {{ |
| 498 | width: 100%; | 744 | width: 100%; |
| 499 | break-inside: avoid; | 745 | break-inside: avoid; |
| 500 | page-break-inside: avoid; | 746 | page-break-inside: avoid; |
| 747 | + /* 表格布局固定 */ | ||
| 748 | + table-layout: fixed; | ||
| 749 | + max-width: 100%; | ||
| 750 | + overflow: hidden; | ||
| 501 | }} | 751 | }} |
| 502 | 752 | ||
| 503 | th {{ | 753 | th {{ |
| 504 | font-size: {cfg.table.font_size_header}px !important; | 754 | font-size: {cfg.table.font_size_header}px !important; |
| 505 | padding: {cfg.table.cell_padding}px !important; | 755 | padding: {cfg.table.cell_padding}px !important; |
| 756 | + /* 表头文字控制 */ | ||
| 757 | + word-break: break-word; | ||
| 758 | + overflow-wrap: break-word; | ||
| 759 | + hyphens: auto; | ||
| 760 | + max-width: 100%; | ||
| 506 | }} | 761 | }} |
| 507 | 762 | ||
| 508 | td {{ | 763 | td {{ |
| 509 | font-size: {cfg.table.font_size_body}px !important; | 764 | font-size: {cfg.table.font_size_body}px !important; |
| 510 | padding: {cfg.table.cell_padding}px !important; | 765 | padding: {cfg.table.cell_padding}px !important; |
| 511 | max-width: {cfg.table.max_cell_width}px; | 766 | max-width: {cfg.table.max_cell_width}px; |
| 767 | + /* 强制换行,防止溢出 */ | ||
| 512 | word-wrap: break-word; | 768 | word-wrap: break-word; |
| 513 | overflow-wrap: break-word; | 769 | overflow-wrap: break-word; |
| 770 | + word-break: break-word; | ||
| 771 | + hyphens: auto; | ||
| 772 | + white-space: normal; | ||
| 514 | }} | 773 | }} |
| 515 | 774 | ||
| 516 | /* 图表优化 */ | 775 | /* 图表优化 */ |
| @@ -520,22 +779,85 @@ td {{ | @@ -520,22 +779,85 @@ td {{ | ||
| 520 | padding: {cfg.chart.padding}px; | 779 | padding: {cfg.chart.padding}px; |
| 521 | break-inside: avoid; | 780 | break-inside: avoid; |
| 522 | page-break-inside: avoid; | 781 | page-break-inside: avoid; |
| 782 | + /* 防止图表溢出 */ | ||
| 783 | + overflow: hidden; | ||
| 784 | + max-width: 100%; | ||
| 785 | + box-sizing: border-box; | ||
| 523 | }} | 786 | }} |
| 524 | 787 | ||
| 525 | .chart-title {{ | 788 | .chart-title {{ |
| 526 | font-size: {cfg.chart.font_size_title}px !important; | 789 | font-size: {cfg.chart.font_size_title}px !important; |
| 790 | + word-break: break-word; | ||
| 791 | +}} | ||
| 792 | + | ||
| 793 | +/* Hero区域的KPI卡片 */ | ||
| 794 | +.hero-kpi {{ | ||
| 795 | + padding: {cfg.kpi_card.padding}px !important; | ||
| 796 | + overflow: hidden; | ||
| 797 | + box-sizing: border-box; | ||
| 798 | +}} | ||
| 799 | + | ||
| 800 | +.hero-kpi .label {{ | ||
| 801 | + font-size: {cfg.kpi_card.font_size_label}px !important; | ||
| 802 | + word-break: break-word; | ||
| 803 | + max-width: 100%; | ||
| 804 | +}} | ||
| 805 | + | ||
| 806 | +.hero-kpi .value {{ | ||
| 807 | + font-size: {cfg.kpi_card.font_size_value}px !important; | ||
| 808 | + word-break: break-word; | ||
| 809 | + overflow-wrap: break-word; | ||
| 810 | + max-width: 100%; | ||
| 527 | }} | 811 | }} |
| 528 | 812 | ||
| 529 | /* 防止标题孤行 */ | 813 | /* 防止标题孤行 */ |
| 530 | h1, h2, h3, h4, h5, h6 {{ | 814 | h1, h2, h3, h4, h5, h6 {{ |
| 531 | break-after: avoid; | 815 | break-after: avoid; |
| 532 | page-break-after: avoid; | 816 | page-break-after: avoid; |
| 817 | + word-break: break-word; | ||
| 818 | + overflow-wrap: break-word; | ||
| 533 | }} | 819 | }} |
| 534 | 820 | ||
| 535 | -/* 确保内容块不被分页 */ | 821 | +/* 确保内容块不被分页且不溢出 */ |
| 536 | .content-block {{ | 822 | .content-block {{ |
| 537 | break-inside: avoid; | 823 | break-inside: avoid; |
| 538 | page-break-inside: avoid; | 824 | page-break-inside: avoid; |
| 825 | + overflow: hidden; | ||
| 826 | + max-width: 100%; | ||
| 827 | +}} | ||
| 828 | + | ||
| 829 | +/* 全局溢出防护 */ | ||
| 830 | +* {{ | ||
| 831 | + box-sizing: border-box; | ||
| 832 | + max-width: 100%; | ||
| 833 | +}} | ||
| 834 | + | ||
| 835 | +/* 特别控制数字和长单词 */ | ||
| 836 | +.kpi-value, .value, .delta {{ | ||
| 837 | + font-variant-numeric: tabular-nums; | ||
| 838 | + letter-spacing: -0.02em; /* 稍微紧缩间距以节省空间 */ | ||
| 839 | +}} | ||
| 840 | + | ||
| 841 | +/* 色块(badge)样式控制 */ | ||
| 842 | +.badge, .callout {{ | ||
| 843 | + display: inline-block; | ||
| 844 | + max-width: 100%; | ||
| 845 | + overflow: hidden; | ||
| 846 | + text-overflow: ellipsis; | ||
| 847 | + white-space: normal; | ||
| 848 | +}} | ||
| 849 | + | ||
| 850 | +/* 响应式调整 */ | ||
| 851 | +@media print {{ | ||
| 852 | + /* 打印时更严格的控制 */ | ||
| 853 | + * {{ | ||
| 854 | + overflow: visible !important; | ||
| 855 | + max-width: 100% !important; | ||
| 856 | + }} | ||
| 857 | + | ||
| 858 | + .kpi-card, .callout, .chart-card {{ | ||
| 859 | + overflow: hidden !important; | ||
| 860 | + }} | ||
| 539 | }} | 861 | }} |
| 540 | """ | 862 | """ |
| 541 | 863 |
-
Please register or login to post a comment