Showing
3 changed files
with
322 additions
and
50 deletions
| @@ -68,6 +68,17 @@ class ChartToSVGConverter: | @@ -68,6 +68,17 @@ class ChartToSVGConverter: | ||
| 68 | 'var(--color-success)': '#50C878', # 翠绿色(从#28A745改为更明亮) | 68 | 'var(--color-success)': '#50C878', # 翠绿色(从#28A745改为更明亮) |
| 69 | 'var(--re-success-color)': '#50C878', # 翠绿色 | 69 | 'var(--re-success-color)': '#50C878', # 翠绿色 |
| 70 | 'var(--re-success-color-translucent)': (0.314, 0.784, 0.471, 0.08), # 绿色极浅透明 rgba(80, 200, 120, 0.08) | 70 | 'var(--re-success-color-translucent)': (0.314, 0.784, 0.471, 0.08), # 绿色极浅透明 rgba(80, 200, 120, 0.08) |
| 71 | + 'var(--color-accent-positive)': '#50C878', | ||
| 72 | + 'var(--color-accent-negative)': '#E85D75', | ||
| 73 | + 'var(--color-text-secondary)': '#6B7280', | ||
| 74 | + 'var(--accentPositive)': '#50C878', | ||
| 75 | + 'var(--accentNegative)': '#E85D75', | ||
| 76 | + 'var(--sentiment-positive, #28A745)': '#28A745', | ||
| 77 | + 'var(--sentiment-negative, #E53E3E)': '#E53E3E', | ||
| 78 | + 'var(--sentiment-neutral, #FFC107)': '#FFC107', | ||
| 79 | + 'var(--sentiment-positive)': '#28A745', | ||
| 80 | + 'var(--sentiment-negative)': '#E53E3E', | ||
| 81 | + 'var(--sentiment-neutral)': '#FFC107', | ||
| 71 | 'var(--color-primary)': '#3498DB', # 天蓝色 | 82 | 'var(--color-primary)': '#3498DB', # 天蓝色 |
| 72 | 'var(--color-secondary)': '#95A5A6', # 浅灰色 | 83 | 'var(--color-secondary)': '#95A5A6', # 浅灰色 |
| 73 | } | 84 | } |
| @@ -225,6 +236,13 @@ class ChartToSVGConverter: | @@ -225,6 +236,13 @@ class ChartToSVGConverter: | ||
| 225 | # 【增强】处理CSS变量,例如 var(--color-accent) | 236 | # 【增强】处理CSS变量,例如 var(--color-accent) |
| 226 | # 使用预定义的颜色映射表替代CSS变量,确保不同变量有不同的颜色 | 237 | # 使用预定义的颜色映射表替代CSS变量,确保不同变量有不同的颜色 |
| 227 | if color.startswith('var('): | 238 | if color.startswith('var('): |
| 239 | + # 解析 var(--token, fallback) 形式 | ||
| 240 | + fb_match = re.match(r'^var\(\s*--[^,)+]+,\s*([^)]+)\)', color) | ||
| 241 | + if fb_match: | ||
| 242 | + fb_raw = fb_match.group(1).strip() | ||
| 243 | + fb_color = self._parse_color(fb_raw) | ||
| 244 | + if fb_color: | ||
| 245 | + return fb_color | ||
| 228 | # 尝试从映射表中查找对应的颜色 | 246 | # 尝试从映射表中查找对应的颜色 |
| 229 | mapped_color = self.CSS_VAR_COLOR_MAP.get(color) | 247 | mapped_color = self.CSS_VAR_COLOR_MAP.get(color) |
| 230 | if mapped_color: | 248 | if mapped_color: |
| @@ -406,7 +424,7 @@ class ChartToSVGConverter: | @@ -406,7 +424,7 @@ class ChartToSVGConverter: | ||
| 406 | 424 | ||
| 407 | # 获取配置 | 425 | # 获取配置 |
| 408 | y_axis_id = dataset.get('yAxisID', 'y') | 426 | y_axis_id = dataset.get('yAxisID', 'y') |
| 409 | - fill = dataset.get('fill', False) | 427 | + fill = True # 强制开启填充,便于对比 |
| 410 | tension = dataset.get('tension', 0) # 0表示直线,0.4表示平滑曲线 | 428 | tension = dataset.get('tension', 0) # 0表示直线,0.4表示平滑曲线 |
| 411 | border_color = self._parse_color(dataset.get('borderColor', color)) | 429 | border_color = self._parse_color(dataset.get('borderColor', color)) |
| 412 | background_color = self._parse_color(dataset.get('backgroundColor', color)) | 430 | background_color = self._parse_color(dataset.get('backgroundColor', color)) |
| @@ -449,7 +467,7 @@ class ChartToSVGConverter: | @@ -449,7 +467,7 @@ class ChartToSVGConverter: | ||
| 449 | color=border_color, linewidth=2, markersize=6) | 467 | color=border_color, linewidth=2, markersize=6) |
| 450 | 468 | ||
| 451 | if fill: | 469 | if fill: |
| 452 | - ax.fill_between(x_data, y_data, alpha=0.08, color=background_color) | 470 | + ax.fill_between(x_data, y_data, alpha=0.2, color=background_color) |
| 453 | 471 | ||
| 454 | for pos, y_val, text in zip(x_data, y_data, annotations): | 472 | for pos, y_val, text in zip(x_data, y_data, annotations): |
| 455 | if text: | 473 | if text: |
| @@ -478,18 +496,18 @@ class ChartToSVGConverter: | @@ -478,18 +496,18 @@ class ChartToSVGConverter: | ||
| 478 | 496 | ||
| 479 | # 如果需要填充(使用极低透明度避免遮挡) | 497 | # 如果需要填充(使用极低透明度避免遮挡) |
| 480 | if fill: | 498 | if fill: |
| 481 | - ax.fill_between(x_smooth, y_smooth, alpha=0.08, color=background_color) | 499 | + ax.fill_between(x_smooth, y_smooth, alpha=0.2, color=background_color) |
| 482 | except: | 500 | except: |
| 483 | # 如果平滑失败,使用普通折线 | 501 | # 如果平滑失败,使用普通折线 |
| 484 | line, = ax.plot(x_data, dataset_data, marker='o', label=label, | 502 | line, = ax.plot(x_data, dataset_data, marker='o', label=label, |
| 485 | color=border_color, linewidth=2, markersize=6) | 503 | color=border_color, linewidth=2, markersize=6) |
| 486 | if fill: | 504 | if fill: |
| 487 | - ax.fill_between(x_data, dataset_data, alpha=0.08, color=background_color) | 505 | + ax.fill_between(x_data, dataset_data, alpha=0.2, color=background_color) |
| 488 | else: | 506 | else: |
| 489 | line, = ax.plot(x_data, dataset_data, marker='o', label=label, | 507 | line, = ax.plot(x_data, dataset_data, marker='o', label=label, |
| 490 | color=border_color, linewidth=2, markersize=6) | 508 | color=border_color, linewidth=2, markersize=6) |
| 491 | if fill: | 509 | if fill: |
| 492 | - ax.fill_between(x_data, dataset_data, alpha=0.08, color=background_color) | 510 | + ax.fill_between(x_data, dataset_data, alpha=0.2, color=background_color) |
| 493 | else: | 511 | else: |
| 494 | # 直线连接(tension=0或scipy不可用) | 512 | # 直线连接(tension=0或scipy不可用) |
| 495 | line, = ax.plot(x_data, dataset_data, marker='o', label=label, | 513 | line, = ax.plot(x_data, dataset_data, marker='o', label=label, |
| @@ -497,7 +515,7 @@ class ChartToSVGConverter: | @@ -497,7 +515,7 @@ class ChartToSVGConverter: | ||
| 497 | 515 | ||
| 498 | # 如果需要填充(使用极低透明度避免遮挡) | 516 | # 如果需要填充(使用极低透明度避免遮挡) |
| 499 | if fill: | 517 | if fill: |
| 500 | - ax.fill_between(x_data, dataset_data, alpha=0.08, color=background_color) | 518 | + ax.fill_between(x_data, dataset_data, alpha=0.2, color=background_color) |
| 501 | 519 | ||
| 502 | # 记录这条线属于哪个轴 | 520 | # 记录这条线属于哪个轴 |
| 503 | axis_lines[y_axis_id].append(line) | 521 | axis_lines[y_axis_id].append(line) |
| @@ -1345,7 +1345,8 @@ class HTMLRenderer: | @@ -1345,7 +1345,8 @@ class HTMLRenderer: | ||
| 1345 | if self._should_skip_overview_kpi(block): | 1345 | if self._should_skip_overview_kpi(block): |
| 1346 | return "" | 1346 | return "" |
| 1347 | cards = "" | 1347 | cards = "" |
| 1348 | - for item in block.get("items", []): | 1348 | + items = block.get("items", []) |
| 1349 | + for item in items: | ||
| 1349 | delta = item.get("delta") | 1350 | delta = item.get("delta") |
| 1350 | delta_tone = item.get("deltaTone") or "neutral" | 1351 | delta_tone = item.get("deltaTone") or "neutral" |
| 1351 | delta_html = f'<span class="delta {delta_tone}">{self._escape_html(delta)}</span>' if delta else "" | 1352 | delta_html = f'<span class="delta {delta_tone}">{self._escape_html(delta)}</span>' if delta else "" |
| @@ -1356,7 +1357,8 @@ class HTMLRenderer: | @@ -1356,7 +1357,8 @@ class HTMLRenderer: | ||
| 1356 | {delta_html} | 1357 | {delta_html} |
| 1357 | </div> | 1358 | </div> |
| 1358 | """ | 1359 | """ |
| 1359 | - return f'<div class="kpi-grid">{cards}</div>' | 1360 | + count_attr = f' data-kpi-count="{len(items)}"' if items else "" |
| 1361 | + return f'<div class="kpi-grid"{count_attr}>{cards}</div>' | ||
| 1360 | 1362 | ||
| 1361 | def _merge_dicts( | 1363 | def _merge_dicts( |
| 1362 | self, base: Dict[str, Any] | None, override: Dict[str, Any] | None | 1364 | self, base: Dict[str, Any] | None, override: Dict[str, Any] | None |
| @@ -2632,21 +2634,41 @@ table th {{ | @@ -2632,21 +2634,41 @@ table th {{ | ||
| 2632 | margin: 20px 0; | 2634 | margin: 20px 0; |
| 2633 | }} | 2635 | }} |
| 2634 | .kpi-card {{ | 2636 | .kpi-card {{ |
| 2637 | + display: flex; | ||
| 2638 | + flex-direction: column; | ||
| 2639 | + gap: 8px; | ||
| 2635 | padding: 16px; | 2640 | padding: 16px; |
| 2636 | border-radius: 12px; | 2641 | border-radius: 12px; |
| 2637 | background: rgba(0,0,0,0.02); | 2642 | background: rgba(0,0,0,0.02); |
| 2638 | border: 1px solid var(--border-color); | 2643 | border: 1px solid var(--border-color); |
| 2644 | + align-items: flex-start; | ||
| 2639 | }} | 2645 | }} |
| 2640 | .kpi-value {{ | 2646 | .kpi-value {{ |
| 2641 | font-size: 2rem; | 2647 | font-size: 2rem; |
| 2642 | font-weight: 700; | 2648 | font-weight: 700; |
| 2649 | + display: flex; | ||
| 2650 | + flex-wrap: wrap; | ||
| 2651 | + gap: 4px 6px; | ||
| 2652 | + line-height: 1.25; | ||
| 2653 | + word-break: break-word; | ||
| 2654 | + overflow-wrap: break-word; | ||
| 2643 | }} | 2655 | }} |
| 2644 | .kpi-label {{ | 2656 | .kpi-label {{ |
| 2645 | color: var(--secondary-color); | 2657 | color: var(--secondary-color); |
| 2658 | + line-height: 1.35; | ||
| 2659 | + word-break: break-word; | ||
| 2660 | + overflow-wrap: break-word; | ||
| 2661 | + max-width: 100%; | ||
| 2646 | }} | 2662 | }} |
| 2647 | .delta.up {{ color: #27ae60; }} | 2663 | .delta.up {{ color: #27ae60; }} |
| 2648 | .delta.down {{ color: #e74c3c; }} | 2664 | .delta.down {{ color: #e74c3c; }} |
| 2649 | .delta.neutral {{ color: var(--secondary-color); }} | 2665 | .delta.neutral {{ color: var(--secondary-color); }} |
| 2666 | +.delta {{ | ||
| 2667 | + display: block; | ||
| 2668 | + line-height: 1.3; | ||
| 2669 | + word-break: break-word; | ||
| 2670 | + overflow-wrap: break-word; | ||
| 2671 | +}} | ||
| 2650 | .chart-card {{ | 2672 | .chart-card {{ |
| 2651 | margin: 30px 0; | 2673 | margin: 30px 0; |
| 2652 | padding: 20px; | 2674 | padding: 20px; |
| @@ -2879,6 +2901,17 @@ const CSS_VAR_COLOR_MAP = { | @@ -2879,6 +2901,17 @@ const CSS_VAR_COLOR_MAP = { | ||
| 2879 | 'var(--color-success)': '#50C878', | 2901 | 'var(--color-success)': '#50C878', |
| 2880 | 'var(--re-success-color)': '#50C878', | 2902 | 'var(--re-success-color)': '#50C878', |
| 2881 | 'var(--re-success-color-translucent)': 'rgba(80, 200, 120, 0.08)', | 2903 | 'var(--re-success-color-translucent)': 'rgba(80, 200, 120, 0.08)', |
| 2904 | + 'var(--color-accent-positive)': '#50C878', | ||
| 2905 | + 'var(--color-accent-negative)': '#E85D75', | ||
| 2906 | + 'var(--color-text-secondary)': '#6B7280', | ||
| 2907 | + 'var(--accentPositive)': '#50C878', | ||
| 2908 | + 'var(--accentNegative)': '#E85D75', | ||
| 2909 | + 'var(--sentiment-positive, #28A745)': '#28A745', | ||
| 2910 | + 'var(--sentiment-negative, #E53E3E)': '#E53E3E', | ||
| 2911 | + 'var(--sentiment-neutral, #FFC107)': '#FFC107', | ||
| 2912 | + 'var(--sentiment-positive)': '#28A745', | ||
| 2913 | + 'var(--sentiment-negative)': '#E53E3E', | ||
| 2914 | + 'var(--sentiment-neutral)': '#FFC107', | ||
| 2882 | 'var(--color-primary)': '#3498DB', | 2915 | 'var(--color-primary)': '#3498DB', |
| 2883 | 'var(--color-secondary)': '#95A5A6' | 2916 | 'var(--color-secondary)': '#95A5A6' |
| 2884 | }; | 2917 | }; |
| @@ -2893,6 +2926,13 @@ function normalizeColorToken(color) { | @@ -2893,6 +2926,13 @@ function normalizeColorToken(color) { | ||
| 2893 | if (typeof color !== 'string') return color; | 2926 | if (typeof color !== 'string') return color; |
| 2894 | const trimmed = color.trim(); | 2927 | const trimmed = color.trim(); |
| 2895 | if (!trimmed) return null; | 2928 | if (!trimmed) return null; |
| 2929 | + // 支持 var(--token, fallback) 形式,优先解析fallback | ||
| 2930 | + const varWithFallback = trimmed.match(/^var\(\s*--[^,)+]+,\s*([^)]+)\)/i); | ||
| 2931 | + if (varWithFallback && varWithFallback[1]) { | ||
| 2932 | + const fallback = varWithFallback[1].trim(); | ||
| 2933 | + const normalizedFallback = normalizeColorToken(fallback); | ||
| 2934 | + if (normalizedFallback) return normalizedFallback; | ||
| 2935 | + } | ||
| 2896 | if (CSS_VAR_COLOR_MAP[trimmed]) { | 2936 | if (CSS_VAR_COLOR_MAP[trimmed]) { |
| 2897 | return CSS_VAR_COLOR_MAP[trimmed]; | 2937 | return CSS_VAR_COLOR_MAP[trimmed]; |
| 2898 | } | 2938 | } |
| @@ -2979,6 +3019,9 @@ function normalizeDatasetColors(payload, chartType) { | @@ -2979,6 +3019,9 @@ function normalizeDatasetColors(payload, chartType) { | ||
| 2979 | 3019 | ||
| 2980 | data.datasets.forEach((dataset, idx) => { | 3020 | data.datasets.forEach((dataset, idx) => { |
| 2981 | if (!isPlainObject(dataset)) return; | 3021 | if (!isPlainObject(dataset)) return; |
| 3022 | + if (type === 'line') { | ||
| 3023 | + dataset.fill = true; // 对折线图强制开启填充,便于区域对比 | ||
| 3024 | + } | ||
| 2982 | const paletteColor = normalizeColorToken(DEFAULT_CHART_COLORS[idx % DEFAULT_CHART_COLORS.length]); | 3025 | const paletteColor = normalizeColorToken(DEFAULT_CHART_COLORS[idx % DEFAULT_CHART_COLORS.length]); |
| 2983 | const borderInput = dataset.borderColor; | 3026 | const borderInput = dataset.borderColor; |
| 2984 | const backgroundInput = dataset.backgroundColor; | 3027 | const backgroundInput = dataset.backgroundColor; |
| @@ -3013,7 +3056,7 @@ function normalizeDatasetColors(payload, chartType) { | @@ -3013,7 +3056,7 @@ function normalizeDatasetColors(payload, chartType) { | ||
| 3013 | } | 3056 | } |
| 3014 | 3057 | ||
| 3015 | const typeAlpha = type === 'line' | 3058 | const typeAlpha = type === 'line' |
| 3016 | - ? (dataset.fill ? 0.08 : 0.12) | 3059 | + ? (dataset.fill ? 0.25 : 0.18) |
| 3017 | : type === 'radar' | 3060 | : type === 'radar' |
| 3018 | ? 0.25 | 3061 | ? 0.25 |
| 3019 | : type === 'scatter' || type === 'bubble' | 3062 | : type === 'scatter' || type === 'bubble' |
| @@ -67,12 +67,23 @@ class ChartLayout: | @@ -67,12 +67,23 @@ class ChartLayout: | ||
| 67 | @dataclass | 67 | @dataclass |
| 68 | class GridLayout: | 68 | class GridLayout: |
| 69 | """网格布局配置""" | 69 | """网格布局配置""" |
| 70 | - columns: int = 2 # 每行列数 | 70 | + columns: int = 3 # 每行列数(正文默认三列) |
| 71 | gap: int = 20 # 间距 | 71 | gap: int = 20 # 间距 |
| 72 | responsive_breakpoint: int = 768 # 响应式断点(宽度) | 72 | responsive_breakpoint: int = 768 # 响应式断点(宽度) |
| 73 | 73 | ||
| 74 | 74 | ||
| 75 | @dataclass | 75 | @dataclass |
| 76 | +class DataBlockLayout: | ||
| 77 | + """数据块(色块、KPI、表格等)的缩放配置""" | ||
| 78 | + overview_text_scale: float = 0.93 # 文章总览数据块文字缩放(轻微缩小) | ||
| 79 | + overview_kpi_scale: float = 0.88 # 总览KPI缩放 | ||
| 80 | + body_text_scale: float = 0.8 # 正文数据块文字缩放(大幅缩小) | ||
| 81 | + body_kpi_scale: float = 0.76 # 正文KPI缩放 | ||
| 82 | + min_overview_font: int = 12 # 总览最小字号 | ||
| 83 | + min_body_font: int = 11 # 正文最小字号 | ||
| 84 | + | ||
| 85 | + | ||
| 86 | +@dataclass | ||
| 76 | class PageLayout: | 87 | class PageLayout: |
| 77 | """页面整体布局配置""" | 88 | """页面整体布局配置""" |
| 78 | font_size_base: int = 14 # 基础字号 | 89 | font_size_base: int = 14 # 基础字号 |
| @@ -96,6 +107,7 @@ class PDFLayoutConfig: | @@ -96,6 +107,7 @@ class PDFLayoutConfig: | ||
| 96 | table: TableLayout | 107 | table: TableLayout |
| 97 | chart: ChartLayout | 108 | chart: ChartLayout |
| 98 | grid: GridLayout | 109 | grid: GridLayout |
| 110 | + data_block: DataBlockLayout | ||
| 99 | 111 | ||
| 100 | # 优化策略配置 | 112 | # 优化策略配置 |
| 101 | auto_adjust_font_size: bool = True # 自动调整字号 | 113 | auto_adjust_font_size: bool = True # 自动调整字号 |
| @@ -112,6 +124,7 @@ class PDFLayoutConfig: | @@ -112,6 +124,7 @@ class PDFLayoutConfig: | ||
| 112 | 'table': asdict(self.table), | 124 | 'table': asdict(self.table), |
| 113 | 'chart': asdict(self.chart), | 125 | 'chart': asdict(self.chart), |
| 114 | 'grid': asdict(self.grid), | 126 | 'grid': asdict(self.grid), |
| 127 | + 'data_block': asdict(self.data_block), | ||
| 115 | 'auto_adjust_font_size': self.auto_adjust_font_size, | 128 | 'auto_adjust_font_size': self.auto_adjust_font_size, |
| 116 | 'auto_adjust_grid_columns': self.auto_adjust_grid_columns, | 129 | 'auto_adjust_grid_columns': self.auto_adjust_grid_columns, |
| 117 | 'prevent_orphan_headers': self.prevent_orphan_headers, | 130 | 'prevent_orphan_headers': self.prevent_orphan_headers, |
| @@ -128,6 +141,7 @@ class PDFLayoutConfig: | @@ -128,6 +141,7 @@ class PDFLayoutConfig: | ||
| 128 | table=TableLayout(**data['table']), | 141 | table=TableLayout(**data['table']), |
| 129 | chart=ChartLayout(**data['chart']), | 142 | chart=ChartLayout(**data['chart']), |
| 130 | grid=GridLayout(**data['grid']), | 143 | grid=GridLayout(**data['grid']), |
| 144 | + data_block=DataBlockLayout(**data.get('data_block', {})), | ||
| 131 | auto_adjust_font_size=data.get('auto_adjust_font_size', True), | 145 | auto_adjust_font_size=data.get('auto_adjust_font_size', True), |
| 132 | auto_adjust_grid_columns=data.get('auto_adjust_grid_columns', True), | 146 | auto_adjust_grid_columns=data.get('auto_adjust_grid_columns', True), |
| 133 | prevent_orphan_headers=data.get('prevent_orphan_headers', True), | 147 | prevent_orphan_headers=data.get('prevent_orphan_headers', True), |
| @@ -174,6 +188,7 @@ class PDFLayoutOptimizer: | @@ -174,6 +188,7 @@ class PDFLayoutOptimizer: | ||
| 174 | table=TableLayout(), | 188 | table=TableLayout(), |
| 175 | chart=ChartLayout(), | 189 | chart=ChartLayout(), |
| 176 | grid=GridLayout(), | 190 | grid=GridLayout(), |
| 191 | + data_block=DataBlockLayout(), | ||
| 177 | ) | 192 | ) |
| 178 | 193 | ||
| 179 | def optimize_for_document(self, document_ir: Dict[str, Any]) -> PDFLayoutConfig: | 194 | def optimize_for_document(self, document_ir: Dict[str, Any]) -> PDFLayoutConfig: |
| @@ -469,6 +484,7 @@ class PDFLayoutOptimizer: | @@ -469,6 +484,7 @@ class PDFLayoutOptimizer: | ||
| 469 | table=TableLayout(**asdict(self.config.table)), | 484 | table=TableLayout(**asdict(self.config.table)), |
| 470 | chart=ChartLayout(**asdict(self.config.chart)), | 485 | chart=ChartLayout(**asdict(self.config.chart)), |
| 471 | grid=GridLayout(**asdict(self.config.grid)), | 486 | grid=GridLayout(**asdict(self.config.grid)), |
| 487 | + data_block=DataBlockLayout(**asdict(self.config.data_block)), | ||
| 472 | auto_adjust_font_size=self.config.auto_adjust_font_size, | 488 | auto_adjust_font_size=self.config.auto_adjust_font_size, |
| 473 | auto_adjust_grid_columns=self.config.auto_adjust_grid_columns, | 489 | auto_adjust_grid_columns=self.config.auto_adjust_grid_columns, |
| 474 | prevent_orphan_headers=self.config.prevent_orphan_headers, | 490 | prevent_orphan_headers=self.config.prevent_orphan_headers, |
| @@ -531,30 +547,88 @@ class PDFLayoutOptimizer: | @@ -531,30 +547,88 @@ class PDFLayoutOptimizer: | ||
| 531 | f"预防性调整字号为{config.kpi_card.font_size_value}px" | 547 | f"预防性调整字号为{config.kpi_card.font_size_value}px" |
| 532 | ) | 548 | ) |
| 533 | 549 | ||
| 534 | - # 根据KPI数量调整网格布局和间距 | 550 | + # 收紧KPI字号上限,为正文数据块缩放留出空间 |
| 551 | + base = config.page.font_size_base | ||
| 552 | + kpi_value_cap = max(base + 6, 20) | ||
| 553 | + kpi_label_cap = max(base - 1, 12) | ||
| 554 | + kpi_change_cap = max(base, 12) | ||
| 555 | + | ||
| 556 | + original_value = config.kpi_card.font_size_value | ||
| 557 | + original_label = config.kpi_card.font_size_label | ||
| 558 | + original_change = config.kpi_card.font_size_change | ||
| 559 | + | ||
| 560 | + config.kpi_card.font_size_value = min(original_value, kpi_value_cap) | ||
| 561 | + config.kpi_card.font_size_value = max(config.kpi_card.font_size_value, base + 1) | ||
| 562 | + config.kpi_card.font_size_label = min(original_label, kpi_label_cap) | ||
| 563 | + config.kpi_card.font_size_label = max(config.kpi_card.font_size_label, 12) | ||
| 564 | + config.kpi_card.font_size_change = min(original_change, kpi_change_cap) | ||
| 565 | + config.kpi_card.font_size_change = max(config.kpi_card.font_size_change, 12) | ||
| 566 | + self.optimization_log.append( | ||
| 567 | + f"KPI字号上限收紧:数值{original_value}px→{config.kpi_card.font_size_value}px," | ||
| 568 | + f"标签{original_label}px→{config.kpi_card.font_size_label}px," | ||
| 569 | + f"变动{original_change}px→{config.kpi_card.font_size_change}px" | ||
| 570 | + ) | ||
| 571 | + | ||
| 572 | + total_blocks = (stats['kpi_count'] + stats['table_count'] + | ||
| 573 | + stats['chart_count'] + stats['callout_count']) | ||
| 574 | + | ||
| 575 | + # 分开收紧文章总览与正文数据块的文字 | ||
| 576 | + if stats['hero_kpi_count'] >= 3 or stats['max_hero_kpi_value_length'] > 6: | ||
| 577 | + prev = config.data_block.overview_kpi_scale | ||
| 578 | + config.data_block.overview_kpi_scale = min(prev, 0.86) | ||
| 579 | + if config.data_block.overview_kpi_scale != prev: | ||
| 580 | + self.optimization_log.append( | ||
| 581 | + f"文章总览KPI较密集,缩放系数 {prev:.2f}→{config.data_block.overview_kpi_scale:.2f}" | ||
| 582 | + ) | ||
| 583 | + | ||
| 584 | + if stats['has_long_text'] or stats['max_table_columns'] > 6: | ||
| 585 | + prev_text = config.data_block.body_text_scale | ||
| 586 | + prev_kpi = config.data_block.body_kpi_scale | ||
| 587 | + config.data_block.body_text_scale = min(prev_text, 0.78) | ||
| 588 | + config.data_block.body_kpi_scale = min(prev_kpi, 0.74) | ||
| 589 | + self.optimization_log.append( | ||
| 590 | + f"正文数据块紧缩:长文本/宽表触发,文字缩放至{config.data_block.body_text_scale*100:.0f}%," | ||
| 591 | + f"KPI缩放至{config.data_block.body_kpi_scale*100:.0f}%" | ||
| 592 | + ) | ||
| 593 | + elif total_blocks > 16: | ||
| 594 | + prev_text = config.data_block.body_text_scale | ||
| 595 | + prev_kpi = config.data_block.body_kpi_scale | ||
| 596 | + config.data_block.body_text_scale = min(prev_text, 0.80) | ||
| 597 | + config.data_block.body_kpi_scale = min(prev_kpi, 0.75) | ||
| 598 | + self.optimization_log.append( | ||
| 599 | + f"正文数据块缩放:内容块较多({total_blocks}个),文字缩放至{config.data_block.body_text_scale*100:.0f}%," | ||
| 600 | + f"KPI缩放至{config.data_block.body_kpi_scale*100:.0f}%" | ||
| 601 | + ) | ||
| 602 | + elif total_blocks > 10: | ||
| 603 | + prev_text = config.data_block.body_text_scale | ||
| 604 | + config.data_block.body_text_scale = min(prev_text, 0.82) | ||
| 605 | + if config.data_block.body_text_scale != prev_text: | ||
| 606 | + self.optimization_log.append( | ||
| 607 | + f"正文数据块轻量缩放({total_blocks}个块),文字缩放系数 {prev_text:.2f}→{config.data_block.body_text_scale:.2f}" | ||
| 608 | + ) | ||
| 609 | + | ||
| 610 | + # 根据KPI数量调整间距但保持正文默认三列装订 | ||
| 611 | + config.grid.columns = 3 | ||
| 535 | if stats['kpi_count'] > 6: | 612 | if stats['kpi_count'] > 6: |
| 536 | - config.grid.columns = 3 | ||
| 537 | config.kpi_card.min_height = 100 | 613 | config.kpi_card.min_height = 100 |
| 538 | config.kpi_card.padding = 14 # 缩小padding以节省空间 | 614 | config.kpi_card.padding = 14 # 缩小padding以节省空间 |
| 539 | config.grid.gap = 16 # 减小间距 | 615 | config.grid.gap = 16 # 减小间距 |
| 540 | self.optimization_log.append( | 616 | self.optimization_log.append( |
| 541 | f"KPI卡片较多({stats['kpi_count']}个)," | 617 | f"KPI卡片较多({stats['kpi_count']}个)," |
| 542 | - f"调整为3列布局并缩小内边距和间距" | 618 | + f"保持三列布局并缩小内边距和间距" |
| 543 | ) | 619 | ) |
| 544 | elif stats['kpi_count'] > 4: | 620 | elif stats['kpi_count'] > 4: |
| 545 | - config.grid.columns = 2 | ||
| 546 | config.kpi_card.padding = 16 | 621 | config.kpi_card.padding = 16 |
| 547 | config.grid.gap = 18 | 622 | config.grid.gap = 18 |
| 548 | self.optimization_log.append( | 623 | self.optimization_log.append( |
| 549 | - f"KPI卡片适中({stats['kpi_count']}个),使用2列布局" | 624 | + f"KPI卡片适中({stats['kpi_count']}个),保持三列布局并适度调整间距" |
| 550 | ) | 625 | ) |
| 551 | elif stats['kpi_count'] <= 2: | 626 | elif stats['kpi_count'] <= 2: |
| 552 | - config.grid.columns = 1 | ||
| 553 | config.kpi_card.padding = 22 # 较少卡片时增加padding | 627 | config.kpi_card.padding = 22 # 较少卡片时增加padding |
| 554 | config.grid.gap = 20 | 628 | config.grid.gap = 20 |
| 555 | self.optimization_log.append( | 629 | self.optimization_log.append( |
| 556 | f"KPI卡片较少({stats['kpi_count']}个)," | 630 | f"KPI卡片较少({stats['kpi_count']}个)," |
| 557 | - f"使用1列布局并增加内边距" | 631 | + f"保持三列布局并增加内边距" |
| 558 | ) | 632 | ) |
| 559 | 633 | ||
| 560 | # 根据表格列数调整字号和间距 | 634 | # 根据表格列数调整字号和间距 |
| @@ -601,8 +675,6 @@ class PDFLayoutOptimizer: | @@ -601,8 +675,6 @@ class PDFLayoutOptimizer: | ||
| 601 | ) | 675 | ) |
| 602 | 676 | ||
| 603 | # 如果内容较多,减小整体字号 | 677 | # 如果内容较多,减小整体字号 |
| 604 | - total_blocks = (stats['kpi_count'] + stats['table_count'] + | ||
| 605 | - stats['chart_count'] + stats['callout_count']) | ||
| 606 | if total_blocks > 20: | 678 | if total_blocks > 20: |
| 607 | config.page.font_size_base = 13 | 679 | config.page.font_size_base = 13 |
| 608 | config.page.font_size_h2 = 22 | 680 | config.page.font_size_h2 = 22 |
| @@ -693,6 +765,32 @@ class PDFLayoutOptimizer: | @@ -693,6 +765,32 @@ class PDFLayoutOptimizer: | ||
| 693 | str: CSS样式字符串 | 765 | str: CSS样式字符串 |
| 694 | """ | 766 | """ |
| 695 | cfg = self.config | 767 | cfg = self.config |
| 768 | + db = cfg.data_block | ||
| 769 | + | ||
| 770 | + def _scaled(value: float, scale: float, minimum: int) -> int: | ||
| 771 | + """按比例缩放并下限保护,避免数据块文字过大或过小""" | ||
| 772 | + try: | ||
| 773 | + return max(int(round(value * scale)), minimum) | ||
| 774 | + except Exception: | ||
| 775 | + return minimum | ||
| 776 | + | ||
| 777 | + # 文章总览数据块字体 | ||
| 778 | + overview_summary_font = _scaled(cfg.page.font_size_base, db.overview_text_scale, db.min_overview_font) | ||
| 779 | + overview_badge_font = _scaled(max(cfg.page.font_size_base - 2, db.min_overview_font), db.overview_text_scale, db.min_overview_font) | ||
| 780 | + overview_kpi_value = _scaled(cfg.kpi_card.font_size_value, db.overview_kpi_scale, db.min_overview_font + 1) | ||
| 781 | + overview_kpi_label = _scaled(cfg.kpi_card.font_size_label, db.overview_kpi_scale, db.min_overview_font) | ||
| 782 | + overview_kpi_delta = _scaled(cfg.kpi_card.font_size_change, db.overview_kpi_scale, db.min_overview_font) | ||
| 783 | + | ||
| 784 | + # 正文数据块字体 | ||
| 785 | + body_kpi_value = _scaled(cfg.kpi_card.font_size_value, db.body_kpi_scale, db.min_body_font + 1) | ||
| 786 | + body_kpi_label = _scaled(cfg.kpi_card.font_size_label, db.body_kpi_scale, db.min_body_font) | ||
| 787 | + body_kpi_delta = _scaled(cfg.kpi_card.font_size_change, db.body_kpi_scale, db.min_body_font) | ||
| 788 | + body_callout_title = _scaled(cfg.callout.font_size_title, db.body_text_scale, db.min_body_font + 1) | ||
| 789 | + body_callout_content = _scaled(cfg.callout.font_size_content, db.body_text_scale, db.min_body_font) | ||
| 790 | + body_table_header = _scaled(cfg.table.font_size_header, db.body_text_scale, db.min_body_font) | ||
| 791 | + body_table_body = _scaled(cfg.table.font_size_body, db.body_text_scale, db.min_body_font) | ||
| 792 | + body_chart_title = _scaled(cfg.chart.font_size_title, db.body_text_scale, db.min_body_font + 1) | ||
| 793 | + body_badge_font = _scaled(max(cfg.page.font_size_base - 2, db.min_body_font), db.body_text_scale, db.min_body_font) | ||
| 696 | 794 | ||
| 697 | css = f""" | 795 | css = f""" |
| 698 | /* PDF布局优化样式 - 由PDFLayoutOptimizer自动生成 */ | 796 | /* PDF布局优化样式 - 由PDFLayoutOptimizer自动生成 */ |
| @@ -734,12 +832,100 @@ p {{ | @@ -734,12 +832,100 @@ p {{ | ||
| 734 | margin-bottom: {cfg.page.section_spacing}px; | 832 | margin-bottom: {cfg.page.section_spacing}px; |
| 735 | }} | 833 | }} |
| 736 | 834 | ||
| 737 | -/* KPI卡片优化 - 防止溢出 */ | 835 | +/* KPI卡片优化 - WeasyPrint不支持CSS Grid,使用Flex实现等宽排布 */ |
| 738 | .kpi-grid {{ | 836 | .kpi-grid {{ |
| 739 | - display: grid; | ||
| 740 | - grid-template-columns: repeat({cfg.grid.columns}, 1fr); | 837 | + display: flex !important; |
| 838 | + flex-wrap: wrap; | ||
| 741 | gap: {cfg.grid.gap}px; | 839 | gap: {cfg.grid.gap}px; |
| 742 | margin: 20px 0; | 840 | margin: 20px 0; |
| 841 | + align-items: stretch; | ||
| 842 | + page-break-inside: avoid !important; | ||
| 843 | + break-inside: avoid !important; | ||
| 844 | + page-break-after: avoid !important; | ||
| 845 | + page-break-before: avoid !important; | ||
| 846 | + break-before: avoid !important; | ||
| 847 | + break-after: avoid !important; | ||
| 848 | +}} | ||
| 849 | + | ||
| 850 | +.kpi-grid .kpi-card {{ | ||
| 851 | + box-sizing: border-box; | ||
| 852 | + flex: 0 1 calc(33.333% - {cfg.grid.gap}px) !important; | ||
| 853 | + max-width: calc(33.333% - {cfg.grid.gap}px) !important; | ||
| 854 | +}} | ||
| 855 | + | ||
| 856 | +/* 单条/双条/三条的特殊列数 */ | ||
| 857 | +.chapter .kpi-grid[data-kpi-count="1"] .kpi-card {{ | ||
| 858 | + flex-basis: 100% !important; | ||
| 859 | + max-width: 100% !important; | ||
| 860 | +}} | ||
| 861 | +.chapter .kpi-grid[data-kpi-count="2"] .kpi-card {{ | ||
| 862 | + flex-basis: calc(50% - {cfg.grid.gap}px) !important; | ||
| 863 | + max-width: calc(50% - {cfg.grid.gap}px) !important; | ||
| 864 | +}} | ||
| 865 | +.chapter .kpi-grid[data-kpi-count="3"] .kpi-card {{ | ||
| 866 | + flex-basis: calc(33.333% - {cfg.grid.gap}px) !important; | ||
| 867 | + max-width: calc(33.333% - {cfg.grid.gap}px) !important; | ||
| 868 | +}} | ||
| 869 | + | ||
| 870 | +/* 四条时采用2x2排布 */ | ||
| 871 | +.chapter .kpi-grid[data-kpi-count="4"] .kpi-card {{ | ||
| 872 | + flex-basis: calc(50% - {cfg.grid.gap}px) !important; | ||
| 873 | + max-width: calc(50% - {cfg.grid.gap}px) !important; | ||
| 874 | +}} | ||
| 875 | +.chapter .kpi-grid[data-kpi-count="4"] {{ | ||
| 876 | + page-break-before: auto !important; | ||
| 877 | + break-before: auto !important; | ||
| 878 | + page-break-inside: avoid !important; | ||
| 879 | + margin-top: 8px !important; | ||
| 880 | +}} | ||
| 881 | + | ||
| 882 | +/* hr 与紧随的KPI/正文保持同页,减少多余空白 */ | ||
| 883 | +hr {{ | ||
| 884 | + page-break-before: avoid !important; | ||
| 885 | + page-break-after: avoid !important; | ||
| 886 | + break-before: avoid !important; | ||
| 887 | + break-after: avoid !important; | ||
| 888 | + margin: 12px 0 !important; | ||
| 889 | +}} | ||
| 890 | + | ||
| 891 | +/* 五条及以上默认三列(6个自动两行3+3) */ | ||
| 892 | +.chapter .kpi-grid[data-kpi-count="5"] .kpi-card, | ||
| 893 | +.chapter .kpi-grid[data-kpi-count="6"] .kpi-card, | ||
| 894 | +.chapter .kpi-grid[data-kpi-count="7"] .kpi-card, | ||
| 895 | +.chapter .kpi-grid[data-kpi-count="8"] .kpi-card, | ||
| 896 | +.chapter .kpi-grid[data-kpi-count="9"] .kpi-card, | ||
| 897 | +.chapter .kpi-grid[data-kpi-count="10"] .kpi-card, | ||
| 898 | +.chapter .kpi-grid[data-kpi-count="11"] .kpi-card, | ||
| 899 | +.chapter .kpi-grid[data-kpi-count="12"] .kpi-card, | ||
| 900 | +.chapter .kpi-grid[data-kpi-count="13"] .kpi-card, | ||
| 901 | +.chapter .kpi-grid[data-kpi-count="14"] .kpi-card, | ||
| 902 | +.chapter .kpi-grid[data-kpi-count="15"] .kpi-card, | ||
| 903 | +.chapter .kpi-grid[data-kpi-count="16"] .kpi-card {{ | ||
| 904 | + flex-basis: calc(33.333% - {cfg.grid.gap}px) !important; | ||
| 905 | + max-width: calc(33.333% - {cfg.grid.gap}px) !important; | ||
| 906 | +}} | ||
| 907 | + | ||
| 908 | +/* 5个时最后两张拉宽为两列 */ | ||
| 909 | +.chapter .kpi-grid[data-kpi-count="5"] .kpi-card:nth-last-child(-n+2) {{ | ||
| 910 | + flex-basis: calc(50% - {cfg.grid.gap}px) !important; | ||
| 911 | + max-width: calc(50% - {cfg.grid.gap}px) !important; | ||
| 912 | +}} | ||
| 913 | + | ||
| 914 | +/* 余数为2时,最后两张平分全宽 */ | ||
| 915 | +.chapter .kpi-grid[data-kpi-count="8"] .kpi-card:nth-last-child(-n+2), | ||
| 916 | +.chapter .kpi-grid[data-kpi-count="11"] .kpi-card:nth-last-child(-n+2), | ||
| 917 | +.chapter .kpi-grid[data-kpi-count="14"] .kpi-card:nth-last-child(-n+2) {{ | ||
| 918 | + flex-basis: calc(50% - {cfg.grid.gap}px) !important; | ||
| 919 | + max-width: calc(50% - {cfg.grid.gap}px) !important; | ||
| 920 | +}} | ||
| 921 | + | ||
| 922 | +/* 余数为1时,最后一张占满全宽 */ | ||
| 923 | +.chapter .kpi-grid[data-kpi-count="7"] .kpi-card:last-child, | ||
| 924 | +.chapter .kpi-grid[data-kpi-count="10"] .kpi-card:last-child, | ||
| 925 | +.chapter .kpi-grid[data-kpi-count="13"] .kpi-card:last-child, | ||
| 926 | +.chapter .kpi-grid[data-kpi-count="16"] .kpi-card:last-child {{ | ||
| 927 | + flex-basis: 100% !important; | ||
| 928 | + max-width: 100% !important; | ||
| 743 | }} | 929 | }} |
| 744 | 930 | ||
| 745 | .kpi-card {{ | 931 | .kpi-card {{ |
| @@ -747,35 +933,39 @@ p {{ | @@ -747,35 +933,39 @@ p {{ | ||
| 747 | min-height: {cfg.kpi_card.min_height}px; | 933 | min-height: {cfg.kpi_card.min_height}px; |
| 748 | break-inside: avoid; | 934 | break-inside: avoid; |
| 749 | page-break-inside: avoid; | 935 | page-break-inside: avoid; |
| 750 | - /* 防止溢出的关键设置 */ | ||
| 751 | - overflow: hidden; | ||
| 752 | box-sizing: border-box; | 936 | box-sizing: border-box; |
| 753 | max-width: 100%; | 937 | max-width: 100%; |
| 938 | + height: auto; | ||
| 939 | + display: flex; | ||
| 940 | + flex-direction: column; | ||
| 941 | + gap: 8px; | ||
| 754 | }} | 942 | }} |
| 755 | 943 | ||
| 756 | -.kpi-card .value {{ | ||
| 757 | - font-size: {cfg.kpi_card.font_size_value}px !important; | ||
| 758 | - line-height: 1.2; | ||
| 759 | - /* 强制换行和溢出控制 */ | 944 | +.kpi-card .kpi-value {{ |
| 945 | + font-size: {body_kpi_value}px !important; | ||
| 946 | + line-height: 1.25; | ||
| 760 | word-break: break-word; | 947 | word-break: break-word; |
| 761 | overflow-wrap: break-word; | 948 | overflow-wrap: break-word; |
| 762 | hyphens: auto; | 949 | hyphens: auto; |
| 763 | max-width: 100%; | 950 | max-width: 100%; |
| 764 | - overflow: hidden; | ||
| 765 | - text-overflow: ellipsis; | ||
| 766 | -}} | ||
| 767 | - | ||
| 768 | -.kpi-card .label {{ | ||
| 769 | - font-size: {cfg.kpi_card.font_size_label}px !important; | ||
| 770 | - /* 防止标签溢出 */ | 951 | + display: flex; |
| 952 | + flex-wrap: wrap; | ||
| 953 | + align-items: baseline; | ||
| 954 | + gap: 4px 6px; | ||
| 955 | +.kpi-card .kpi-label {{ | ||
| 956 | + font-size: {body_kpi_label}px !important; | ||
| 771 | word-break: break-word; | 957 | word-break: break-word; |
| 772 | overflow-wrap: break-word; | 958 | overflow-wrap: break-word; |
| 773 | max-width: 100%; | 959 | max-width: 100%; |
| 960 | + line-height: 1.35; | ||
| 774 | }} | 961 | }} |
| 775 | 962 | ||
| 776 | -.kpi-card .change {{ | ||
| 777 | - font-size: {cfg.kpi_card.font_size_change}px !important; | 963 | +.kpi-card .change, |
| 964 | +.kpi-card .delta {{ | ||
| 965 | + font-size: {body_kpi_delta}px !important; | ||
| 778 | word-break: break-word; | 966 | word-break: break-word; |
| 967 | + overflow-wrap: break-word; | ||
| 968 | + line-height: 1.3; | ||
| 779 | }} | 969 | }} |
| 780 | 970 | ||
| 781 | /* 提示框优化 - 防止溢出 */ | 971 | /* 提示框优化 - 防止溢出 */ |
| @@ -783,6 +973,7 @@ p {{ | @@ -783,6 +973,7 @@ p {{ | ||
| 783 | padding: {cfg.callout.padding}px !important; | 973 | padding: {cfg.callout.padding}px !important; |
| 784 | margin: 20px 0; | 974 | margin: 20px 0; |
| 785 | line-height: {cfg.callout.line_height}; | 975 | line-height: {cfg.callout.line_height}; |
| 976 | + font-size: {body_callout_content}px !important; | ||
| 786 | break-inside: avoid; | 977 | break-inside: avoid; |
| 787 | page-break-inside: avoid; | 978 | page-break-inside: avoid; |
| 788 | /* 防止溢出 */ | 979 | /* 防止溢出 */ |
| @@ -792,19 +983,31 @@ p {{ | @@ -792,19 +983,31 @@ p {{ | ||
| 792 | }} | 983 | }} |
| 793 | 984 | ||
| 794 | .callout-title {{ | 985 | .callout-title {{ |
| 795 | - font-size: {cfg.callout.font_size_title}px !important; | 986 | + font-size: {body_callout_title}px !important; |
| 796 | margin-bottom: 10px; | 987 | margin-bottom: 10px; |
| 797 | word-break: break-word; | 988 | word-break: break-word; |
| 798 | line-height: 1.4; | 989 | line-height: 1.4; |
| 799 | }} | 990 | }} |
| 800 | 991 | ||
| 801 | .callout-content {{ | 992 | .callout-content {{ |
| 802 | - font-size: {cfg.callout.font_size_content}px !important; | 993 | + font-size: {body_callout_content}px !important; |
| 803 | word-break: break-word; | 994 | word-break: break-word; |
| 804 | overflow-wrap: break-word; | 995 | overflow-wrap: break-word; |
| 805 | line-height: {cfg.callout.line_height}; | 996 | line-height: {cfg.callout.line_height}; |
| 806 | }} | 997 | }} |
| 807 | 998 | ||
| 999 | +.callout strong {{ | ||
| 1000 | + font-size: {body_callout_title}px !important; | ||
| 1001 | +}} | ||
| 1002 | + | ||
| 1003 | +.callout p, | ||
| 1004 | +.callout li, | ||
| 1005 | +.callout table, | ||
| 1006 | +.callout td, | ||
| 1007 | +.callout th {{ | ||
| 1008 | + font-size: {body_callout_content}px !important; | ||
| 1009 | +}} | ||
| 1010 | + | ||
| 808 | /* 确保 callout 内部最后一个元素不会溢出底部 */ | 1011 | /* 确保 callout 内部最后一个元素不会溢出底部 */ |
| 809 | .callout > *:last-child, | 1012 | .callout > *:last-child, |
| 810 | .callout > *:last-child > *:last-child {{ | 1013 | .callout > *:last-child > *:last-child {{ |
| @@ -824,7 +1027,7 @@ table {{ | @@ -824,7 +1027,7 @@ table {{ | ||
| 824 | }} | 1027 | }} |
| 825 | 1028 | ||
| 826 | th {{ | 1029 | th {{ |
| 827 | - font-size: {cfg.table.font_size_header}px !important; | 1030 | + font-size: {body_table_header}px !important; |
| 828 | padding: {cfg.table.cell_padding}px !important; | 1031 | padding: {cfg.table.cell_padding}px !important; |
| 829 | /* 表头文字控制 */ | 1032 | /* 表头文字控制 */ |
| 830 | word-break: break-word; | 1033 | word-break: break-word; |
| @@ -834,7 +1037,7 @@ th {{ | @@ -834,7 +1037,7 @@ th {{ | ||
| 834 | }} | 1037 | }} |
| 835 | 1038 | ||
| 836 | td {{ | 1039 | td {{ |
| 837 | - font-size: {cfg.table.font_size_body}px !important; | 1040 | + font-size: {body_table_body}px !important; |
| 838 | padding: {cfg.table.cell_padding}px !important; | 1041 | padding: {cfg.table.cell_padding}px !important; |
| 839 | max-width: {cfg.table.max_cell_width}px; | 1042 | max-width: {cfg.table.max_cell_width}px; |
| 840 | /* 强制换行,防止溢出 */ | 1043 | /* 强制换行,防止溢出 */ |
| @@ -859,7 +1062,7 @@ td {{ | @@ -859,7 +1062,7 @@ td {{ | ||
| 859 | }} | 1062 | }} |
| 860 | 1063 | ||
| 861 | .chart-title {{ | 1064 | .chart-title {{ |
| 862 | - font-size: {cfg.chart.font_size_title}px !important; | 1065 | + font-size: {body_chart_title}px !important; |
| 863 | word-break: break-word; | 1066 | word-break: break-word; |
| 864 | }} | 1067 | }} |
| 865 | 1068 | ||
| @@ -928,19 +1131,26 @@ td {{ | @@ -928,19 +1131,26 @@ td {{ | ||
| 928 | .hero-side {{ | 1131 | .hero-side {{ |
| 929 | flex: 3; /* 右侧占30% */ | 1132 | flex: 3; /* 右侧占30% */ |
| 930 | min-width: 0; | 1133 | min-width: 0; |
| 1134 | + min-height: 0; | ||
| 931 | display: flex; | 1135 | display: flex; |
| 932 | - flex-direction: column; | 1136 | + flex-wrap: wrap; |
| 933 | gap: {max(cfg.grid.gap - 2, 10)}px; | 1137 | gap: {max(cfg.grid.gap - 2, 10)}px; |
| 934 | overflow: hidden; | 1138 | overflow: hidden; |
| 935 | box-sizing: border-box; | 1139 | box-sizing: border-box; |
| 1140 | + width: 100%; | ||
| 936 | }} | 1141 | }} |
| 937 | 1142 | ||
| 938 | /* Hero区域的KPI卡片 - 横向拉长,每行显示一个内容 */ | 1143 | /* Hero区域的KPI卡片 - 横向拉长,每行显示一个内容 */ |
| 939 | .hero-kpi {{ | 1144 | .hero-kpi {{ |
| 1145 | + background: #ffffff; | ||
| 1146 | + border-radius: 16px !important; | ||
| 1147 | + border: 1px solid rgba(0, 0, 0, 0.06); | ||
| 1148 | + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.08); | ||
| 1149 | + flex: 0 1 calc(50% - {max(cfg.grid.gap - 2, 10)}px); | ||
| 1150 | + max-width: calc(50% - {max(cfg.grid.gap - 2, 10)}px); | ||
| 940 | padding: 12px 18px !important; /* 增加横向padding */ | 1151 | padding: 12px 18px !important; /* 增加横向padding */ |
| 941 | overflow: hidden; | 1152 | overflow: hidden; |
| 942 | box-sizing: border-box; | 1153 | box-sizing: border-box; |
| 943 | - max-width: 100%; | ||
| 944 | min-height: 85px; /* 增加高度以容纳三行 */ | 1154 | min-height: 85px; /* 增加高度以容纳三行 */ |
| 945 | display: flex; | 1155 | display: flex; |
| 946 | flex-direction: column; | 1156 | flex-direction: column; |
| @@ -948,7 +1158,7 @@ td {{ | @@ -948,7 +1158,7 @@ td {{ | ||
| 948 | }} | 1158 | }} |
| 949 | 1159 | ||
| 950 | .hero-kpi .label {{ | 1160 | .hero-kpi .label {{ |
| 951 | - font-size: {max(cfg.kpi_card.font_size_label - 3, 9)}px !important; /* 减小标签字号 */ | 1161 | + font-size: {overview_kpi_label}px !important; /* 适度减小标签字号 */ |
| 952 | word-break: break-word; | 1162 | word-break: break-word; |
| 953 | max-width: 100%; | 1163 | max-width: 100%; |
| 954 | line-height: 1.2; | 1164 | line-height: 1.2; |
| @@ -959,7 +1169,7 @@ td {{ | @@ -959,7 +1169,7 @@ td {{ | ||
| 959 | }} | 1169 | }} |
| 960 | 1170 | ||
| 961 | .hero-kpi .value {{ | 1171 | .hero-kpi .value {{ |
| 962 | - font-size: {max(cfg.kpi_card.font_size_value - 12, 14)}px !important; /* 减小数值字号 */ | 1172 | + font-size: {overview_kpi_value}px !important; /* 适度减小数值字号 */ |
| 963 | word-break: break-word; | 1173 | word-break: break-word; |
| 964 | overflow-wrap: break-word; | 1174 | overflow-wrap: break-word; |
| 965 | max-width: 100%; | 1175 | max-width: 100%; |
| @@ -972,7 +1182,7 @@ td {{ | @@ -972,7 +1182,7 @@ td {{ | ||
| 972 | }} | 1182 | }} |
| 973 | 1183 | ||
| 974 | .hero-kpi .delta {{ | 1184 | .hero-kpi .delta {{ |
| 975 | - font-size: {max(cfg.kpi_card.font_size_change - 3, 9)}px !important; /* 减小变化值字号 */ | 1185 | + font-size: {overview_kpi_delta}px !important; /* 适度减小变化值字号 */ |
| 976 | word-break: break-word; | 1186 | word-break: break-word; |
| 977 | margin-top: 3px; | 1187 | margin-top: 3px; |
| 978 | display: block; /* 独占一行 */ | 1188 | display: block; /* 独占一行 */ |
| @@ -984,7 +1194,7 @@ td {{ | @@ -984,7 +1194,7 @@ td {{ | ||
| 984 | 1194 | ||
| 985 | /* Hero summary文本 */ | 1195 | /* Hero summary文本 */ |
| 986 | .hero-summary {{ | 1196 | .hero-summary {{ |
| 987 | - font-size: {cfg.page.font_size_base}px !important; | 1197 | + font-size: {overview_summary_font}px !important; |
| 988 | line-height: 1.65; | 1198 | line-height: 1.65; |
| 989 | margin-top: 0; | 1199 | margin-top: 0; |
| 990 | margin-bottom: 18px; /* 增加底部边距,与badges保持一致 */ | 1200 | margin-bottom: 18px; /* 增加底部边距,与badges保持一致 */ |
| @@ -1014,7 +1224,7 @@ td {{ | @@ -1014,7 +1224,7 @@ td {{ | ||
| 1014 | 1224 | ||
| 1015 | /* hero highlights中的badge - 拉长加宽的椭圆形背景,与上方文本对齐 */ | 1225 | /* hero highlights中的badge - 拉长加宽的椭圆形背景,与上方文本对齐 */ |
| 1016 | .hero-highlights .badge {{ | 1226 | .hero-highlights .badge {{ |
| 1017 | - font-size: {max(cfg.callout.font_size_content - 3, 10)}px !important; | 1227 | + font-size: {overview_badge_font}px !important; |
| 1018 | padding: 10px 20px !important; /* 增加padding,更好的视觉效果 */ | 1228 | padding: 10px 20px !important; /* 增加padding,更好的视觉效果 */ |
| 1019 | max-width: 100%; | 1229 | max-width: 100%; |
| 1020 | width: 98%; /* 占满宽度,与summary文本对齐 */ | 1230 | width: 98%; /* 占满宽度,与summary文本对齐 */ |
| @@ -1105,7 +1315,7 @@ main > .chapter:first-of-type {{ | @@ -1105,7 +1315,7 @@ main > .chapter:first-of-type {{ | ||
| 1105 | white-space: normal; | 1315 | white-space: normal; |
| 1106 | /* 限制badge的最大尺寸 */ | 1316 | /* 限制badge的最大尺寸 */ |
| 1107 | padding: 4px 12px !important; | 1317 | padding: 4px 12px !important; |
| 1108 | - font-size: {max(cfg.page.font_size_base - 2, 12)}px !important; | 1318 | + font-size: {body_badge_font}px !important; |
| 1109 | line-height: 1.4 !important; | 1319 | line-height: 1.4 !important; |
| 1110 | /* 防止badge异常过大 */ | 1320 | /* 防止badge异常过大 */ |
| 1111 | word-break: break-word; | 1321 | word-break: break-word; |
| @@ -1147,4 +1357,5 @@ __all__ = [ | @@ -1147,4 +1357,5 @@ __all__ = [ | ||
| 1147 | 'TableLayout', | 1357 | 'TableLayout', |
| 1148 | 'ChartLayout', | 1358 | 'ChartLayout', |
| 1149 | 'GridLayout', | 1359 | 'GridLayout', |
| 1360 | + 'DataBlockLayout', | ||
| 1150 | ] | 1361 | ] |
-
Please register or login to post a comment