diff --git a/ReportEngine/renderers/chart_to_svg.py b/ReportEngine/renderers/chart_to_svg.py
index 6dd4031..f7dc302 100644
--- a/ReportEngine/renderers/chart_to_svg.py
+++ b/ReportEngine/renderers/chart_to_svg.py
@@ -68,6 +68,17 @@ class ChartToSVGConverter:
'var(--color-success)': '#50C878', # 翠绿色(从#28A745改为更明亮)
'var(--re-success-color)': '#50C878', # 翠绿色
'var(--re-success-color-translucent)': (0.314, 0.784, 0.471, 0.08), # 绿色极浅透明 rgba(80, 200, 120, 0.08)
+ 'var(--color-accent-positive)': '#50C878',
+ 'var(--color-accent-negative)': '#E85D75',
+ 'var(--color-text-secondary)': '#6B7280',
+ 'var(--accentPositive)': '#50C878',
+ 'var(--accentNegative)': '#E85D75',
+ 'var(--sentiment-positive, #28A745)': '#28A745',
+ 'var(--sentiment-negative, #E53E3E)': '#E53E3E',
+ 'var(--sentiment-neutral, #FFC107)': '#FFC107',
+ 'var(--sentiment-positive)': '#28A745',
+ 'var(--sentiment-negative)': '#E53E3E',
+ 'var(--sentiment-neutral)': '#FFC107',
'var(--color-primary)': '#3498DB', # 天蓝色
'var(--color-secondary)': '#95A5A6', # 浅灰色
}
@@ -225,6 +236,13 @@ class ChartToSVGConverter:
# 【增强】处理CSS变量,例如 var(--color-accent)
# 使用预定义的颜色映射表替代CSS变量,确保不同变量有不同的颜色
if color.startswith('var('):
+ # 解析 var(--token, fallback) 形式
+ fb_match = re.match(r'^var\(\s*--[^,)+]+,\s*([^)]+)\)', color)
+ if fb_match:
+ fb_raw = fb_match.group(1).strip()
+ fb_color = self._parse_color(fb_raw)
+ if fb_color:
+ return fb_color
# 尝试从映射表中查找对应的颜色
mapped_color = self.CSS_VAR_COLOR_MAP.get(color)
if mapped_color:
@@ -406,7 +424,7 @@ class ChartToSVGConverter:
# 获取配置
y_axis_id = dataset.get('yAxisID', 'y')
- fill = dataset.get('fill', False)
+ fill = True # 强制开启填充,便于对比
tension = dataset.get('tension', 0) # 0表示直线,0.4表示平滑曲线
border_color = self._parse_color(dataset.get('borderColor', color))
background_color = self._parse_color(dataset.get('backgroundColor', color))
@@ -449,7 +467,7 @@ class ChartToSVGConverter:
color=border_color, linewidth=2, markersize=6)
if fill:
- ax.fill_between(x_data, y_data, alpha=0.08, color=background_color)
+ ax.fill_between(x_data, y_data, alpha=0.2, color=background_color)
for pos, y_val, text in zip(x_data, y_data, annotations):
if text:
@@ -478,18 +496,18 @@ class ChartToSVGConverter:
# 如果需要填充(使用极低透明度避免遮挡)
if fill:
- ax.fill_between(x_smooth, y_smooth, alpha=0.08, color=background_color)
+ ax.fill_between(x_smooth, y_smooth, alpha=0.2, color=background_color)
except:
# 如果平滑失败,使用普通折线
line, = ax.plot(x_data, dataset_data, marker='o', label=label,
color=border_color, linewidth=2, markersize=6)
if fill:
- ax.fill_between(x_data, dataset_data, alpha=0.08, color=background_color)
+ ax.fill_between(x_data, dataset_data, alpha=0.2, color=background_color)
else:
line, = ax.plot(x_data, dataset_data, marker='o', label=label,
color=border_color, linewidth=2, markersize=6)
if fill:
- ax.fill_between(x_data, dataset_data, alpha=0.08, color=background_color)
+ ax.fill_between(x_data, dataset_data, alpha=0.2, color=background_color)
else:
# 直线连接(tension=0或scipy不可用)
line, = ax.plot(x_data, dataset_data, marker='o', label=label,
@@ -497,7 +515,7 @@ class ChartToSVGConverter:
# 如果需要填充(使用极低透明度避免遮挡)
if fill:
- ax.fill_between(x_data, dataset_data, alpha=0.08, color=background_color)
+ ax.fill_between(x_data, dataset_data, alpha=0.2, color=background_color)
# 记录这条线属于哪个轴
axis_lines[y_axis_id].append(line)
diff --git a/ReportEngine/renderers/html_renderer.py b/ReportEngine/renderers/html_renderer.py
index 13d126c..b5cf2be 100644
--- a/ReportEngine/renderers/html_renderer.py
+++ b/ReportEngine/renderers/html_renderer.py
@@ -1345,7 +1345,8 @@ class HTMLRenderer:
if self._should_skip_overview_kpi(block):
return ""
cards = ""
- for item in block.get("items", []):
+ items = block.get("items", [])
+ for item in items:
delta = item.get("delta")
delta_tone = item.get("deltaTone") or "neutral"
delta_html = f'{self._escape_html(delta)}' if delta else ""
@@ -1356,7 +1357,8 @@ class HTMLRenderer:
{delta_html}
"""
- return f'
{cards}
'
+ count_attr = f' data-kpi-count="{len(items)}"' if items else ""
+ return f'{cards}
'
def _merge_dicts(
self, base: Dict[str, Any] | None, override: Dict[str, Any] | None
@@ -2632,21 +2634,41 @@ table th {{
margin: 20px 0;
}}
.kpi-card {{
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
padding: 16px;
border-radius: 12px;
background: rgba(0,0,0,0.02);
border: 1px solid var(--border-color);
+ align-items: flex-start;
}}
.kpi-value {{
font-size: 2rem;
font-weight: 700;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 6px;
+ line-height: 1.25;
+ word-break: break-word;
+ overflow-wrap: break-word;
}}
.kpi-label {{
color: var(--secondary-color);
+ line-height: 1.35;
+ word-break: break-word;
+ overflow-wrap: break-word;
+ max-width: 100%;
}}
.delta.up {{ color: #27ae60; }}
.delta.down {{ color: #e74c3c; }}
.delta.neutral {{ color: var(--secondary-color); }}
+.delta {{
+ display: block;
+ line-height: 1.3;
+ word-break: break-word;
+ overflow-wrap: break-word;
+}}
.chart-card {{
margin: 30px 0;
padding: 20px;
@@ -2879,6 +2901,17 @@ const CSS_VAR_COLOR_MAP = {
'var(--color-success)': '#50C878',
'var(--re-success-color)': '#50C878',
'var(--re-success-color-translucent)': 'rgba(80, 200, 120, 0.08)',
+ 'var(--color-accent-positive)': '#50C878',
+ 'var(--color-accent-negative)': '#E85D75',
+ 'var(--color-text-secondary)': '#6B7280',
+ 'var(--accentPositive)': '#50C878',
+ 'var(--accentNegative)': '#E85D75',
+ 'var(--sentiment-positive, #28A745)': '#28A745',
+ 'var(--sentiment-negative, #E53E3E)': '#E53E3E',
+ 'var(--sentiment-neutral, #FFC107)': '#FFC107',
+ 'var(--sentiment-positive)': '#28A745',
+ 'var(--sentiment-negative)': '#E53E3E',
+ 'var(--sentiment-neutral)': '#FFC107',
'var(--color-primary)': '#3498DB',
'var(--color-secondary)': '#95A5A6'
};
@@ -2893,6 +2926,13 @@ function normalizeColorToken(color) {
if (typeof color !== 'string') return color;
const trimmed = color.trim();
if (!trimmed) return null;
+ // 支持 var(--token, fallback) 形式,优先解析fallback
+ const varWithFallback = trimmed.match(/^var\(\s*--[^,)+]+,\s*([^)]+)\)/i);
+ if (varWithFallback && varWithFallback[1]) {
+ const fallback = varWithFallback[1].trim();
+ const normalizedFallback = normalizeColorToken(fallback);
+ if (normalizedFallback) return normalizedFallback;
+ }
if (CSS_VAR_COLOR_MAP[trimmed]) {
return CSS_VAR_COLOR_MAP[trimmed];
}
@@ -2979,6 +3019,9 @@ function normalizeDatasetColors(payload, chartType) {
data.datasets.forEach((dataset, idx) => {
if (!isPlainObject(dataset)) return;
+ if (type === 'line') {
+ dataset.fill = true; // 对折线图强制开启填充,便于区域对比
+ }
const paletteColor = normalizeColorToken(DEFAULT_CHART_COLORS[idx % DEFAULT_CHART_COLORS.length]);
const borderInput = dataset.borderColor;
const backgroundInput = dataset.backgroundColor;
@@ -3013,7 +3056,7 @@ function normalizeDatasetColors(payload, chartType) {
}
const typeAlpha = type === 'line'
- ? (dataset.fill ? 0.08 : 0.12)
+ ? (dataset.fill ? 0.25 : 0.18)
: type === 'radar'
? 0.25
: type === 'scatter' || type === 'bubble'
diff --git a/ReportEngine/renderers/pdf_layout_optimizer.py b/ReportEngine/renderers/pdf_layout_optimizer.py
index 0e56ed8..e1dd96a 100644
--- a/ReportEngine/renderers/pdf_layout_optimizer.py
+++ b/ReportEngine/renderers/pdf_layout_optimizer.py
@@ -67,11 +67,22 @@ class ChartLayout:
@dataclass
class GridLayout:
"""网格布局配置"""
- columns: int = 2 # 每行列数
+ columns: int = 3 # 每行列数(正文默认三列)
gap: int = 20 # 间距
responsive_breakpoint: int = 768 # 响应式断点(宽度)
+@dataclass
+class DataBlockLayout:
+ """数据块(色块、KPI、表格等)的缩放配置"""
+ overview_text_scale: float = 0.93 # 文章总览数据块文字缩放(轻微缩小)
+ overview_kpi_scale: float = 0.88 # 总览KPI缩放
+ body_text_scale: float = 0.8 # 正文数据块文字缩放(大幅缩小)
+ body_kpi_scale: float = 0.76 # 正文KPI缩放
+ min_overview_font: int = 12 # 总览最小字号
+ min_body_font: int = 11 # 正文最小字号
+
+
@dataclass
class PageLayout:
"""页面整体布局配置"""
@@ -96,6 +107,7 @@ class PDFLayoutConfig:
table: TableLayout
chart: ChartLayout
grid: GridLayout
+ data_block: DataBlockLayout
# 优化策略配置
auto_adjust_font_size: bool = True # 自动调整字号
@@ -112,6 +124,7 @@ class PDFLayoutConfig:
'table': asdict(self.table),
'chart': asdict(self.chart),
'grid': asdict(self.grid),
+ 'data_block': asdict(self.data_block),
'auto_adjust_font_size': self.auto_adjust_font_size,
'auto_adjust_grid_columns': self.auto_adjust_grid_columns,
'prevent_orphan_headers': self.prevent_orphan_headers,
@@ -128,6 +141,7 @@ class PDFLayoutConfig:
table=TableLayout(**data['table']),
chart=ChartLayout(**data['chart']),
grid=GridLayout(**data['grid']),
+ data_block=DataBlockLayout(**data.get('data_block', {})),
auto_adjust_font_size=data.get('auto_adjust_font_size', True),
auto_adjust_grid_columns=data.get('auto_adjust_grid_columns', True),
prevent_orphan_headers=data.get('prevent_orphan_headers', True),
@@ -174,6 +188,7 @@ class PDFLayoutOptimizer:
table=TableLayout(),
chart=ChartLayout(),
grid=GridLayout(),
+ data_block=DataBlockLayout(),
)
def optimize_for_document(self, document_ir: Dict[str, Any]) -> PDFLayoutConfig:
@@ -469,6 +484,7 @@ class PDFLayoutOptimizer:
table=TableLayout(**asdict(self.config.table)),
chart=ChartLayout(**asdict(self.config.chart)),
grid=GridLayout(**asdict(self.config.grid)),
+ data_block=DataBlockLayout(**asdict(self.config.data_block)),
auto_adjust_font_size=self.config.auto_adjust_font_size,
auto_adjust_grid_columns=self.config.auto_adjust_grid_columns,
prevent_orphan_headers=self.config.prevent_orphan_headers,
@@ -531,30 +547,88 @@ class PDFLayoutOptimizer:
f"预防性调整字号为{config.kpi_card.font_size_value}px"
)
- # 根据KPI数量调整网格布局和间距
+ # 收紧KPI字号上限,为正文数据块缩放留出空间
+ base = config.page.font_size_base
+ kpi_value_cap = max(base + 6, 20)
+ kpi_label_cap = max(base - 1, 12)
+ kpi_change_cap = max(base, 12)
+
+ original_value = config.kpi_card.font_size_value
+ original_label = config.kpi_card.font_size_label
+ original_change = config.kpi_card.font_size_change
+
+ config.kpi_card.font_size_value = min(original_value, kpi_value_cap)
+ config.kpi_card.font_size_value = max(config.kpi_card.font_size_value, base + 1)
+ config.kpi_card.font_size_label = min(original_label, kpi_label_cap)
+ config.kpi_card.font_size_label = max(config.kpi_card.font_size_label, 12)
+ config.kpi_card.font_size_change = min(original_change, kpi_change_cap)
+ config.kpi_card.font_size_change = max(config.kpi_card.font_size_change, 12)
+ self.optimization_log.append(
+ f"KPI字号上限收紧:数值{original_value}px→{config.kpi_card.font_size_value}px,"
+ f"标签{original_label}px→{config.kpi_card.font_size_label}px,"
+ f"变动{original_change}px→{config.kpi_card.font_size_change}px"
+ )
+
+ total_blocks = (stats['kpi_count'] + stats['table_count'] +
+ stats['chart_count'] + stats['callout_count'])
+
+ # 分开收紧文章总览与正文数据块的文字
+ if stats['hero_kpi_count'] >= 3 or stats['max_hero_kpi_value_length'] > 6:
+ prev = config.data_block.overview_kpi_scale
+ config.data_block.overview_kpi_scale = min(prev, 0.86)
+ if config.data_block.overview_kpi_scale != prev:
+ self.optimization_log.append(
+ f"文章总览KPI较密集,缩放系数 {prev:.2f}→{config.data_block.overview_kpi_scale:.2f}"
+ )
+
+ if stats['has_long_text'] or stats['max_table_columns'] > 6:
+ prev_text = config.data_block.body_text_scale
+ prev_kpi = config.data_block.body_kpi_scale
+ config.data_block.body_text_scale = min(prev_text, 0.78)
+ config.data_block.body_kpi_scale = min(prev_kpi, 0.74)
+ self.optimization_log.append(
+ f"正文数据块紧缩:长文本/宽表触发,文字缩放至{config.data_block.body_text_scale*100:.0f}%,"
+ f"KPI缩放至{config.data_block.body_kpi_scale*100:.0f}%"
+ )
+ elif total_blocks > 16:
+ prev_text = config.data_block.body_text_scale
+ prev_kpi = config.data_block.body_kpi_scale
+ config.data_block.body_text_scale = min(prev_text, 0.80)
+ config.data_block.body_kpi_scale = min(prev_kpi, 0.75)
+ self.optimization_log.append(
+ f"正文数据块缩放:内容块较多({total_blocks}个),文字缩放至{config.data_block.body_text_scale*100:.0f}%,"
+ f"KPI缩放至{config.data_block.body_kpi_scale*100:.0f}%"
+ )
+ elif total_blocks > 10:
+ prev_text = config.data_block.body_text_scale
+ config.data_block.body_text_scale = min(prev_text, 0.82)
+ if config.data_block.body_text_scale != prev_text:
+ self.optimization_log.append(
+ f"正文数据块轻量缩放({total_blocks}个块),文字缩放系数 {prev_text:.2f}→{config.data_block.body_text_scale:.2f}"
+ )
+
+ # 根据KPI数量调整间距但保持正文默认三列装订
+ config.grid.columns = 3
if stats['kpi_count'] > 6:
- config.grid.columns = 3
config.kpi_card.min_height = 100
config.kpi_card.padding = 14 # 缩小padding以节省空间
config.grid.gap = 16 # 减小间距
self.optimization_log.append(
f"KPI卡片较多({stats['kpi_count']}个),"
- f"调整为3列布局并缩小内边距和间距"
+ f"保持三列布局并缩小内边距和间距"
)
elif stats['kpi_count'] > 4:
- config.grid.columns = 2
config.kpi_card.padding = 16
config.grid.gap = 18
self.optimization_log.append(
- f"KPI卡片适中({stats['kpi_count']}个),使用2列布局"
+ f"KPI卡片适中({stats['kpi_count']}个),保持三列布局并适度调整间距"
)
elif stats['kpi_count'] <= 2:
- config.grid.columns = 1
config.kpi_card.padding = 22 # 较少卡片时增加padding
config.grid.gap = 20
self.optimization_log.append(
f"KPI卡片较少({stats['kpi_count']}个),"
- f"使用1列布局并增加内边距"
+ f"保持三列布局并增加内边距"
)
# 根据表格列数调整字号和间距
@@ -601,8 +675,6 @@ class PDFLayoutOptimizer:
)
# 如果内容较多,减小整体字号
- total_blocks = (stats['kpi_count'] + stats['table_count'] +
- stats['chart_count'] + stats['callout_count'])
if total_blocks > 20:
config.page.font_size_base = 13
config.page.font_size_h2 = 22
@@ -693,6 +765,32 @@ class PDFLayoutOptimizer:
str: CSS样式字符串
"""
cfg = self.config
+ db = cfg.data_block
+
+ def _scaled(value: float, scale: float, minimum: int) -> int:
+ """按比例缩放并下限保护,避免数据块文字过大或过小"""
+ try:
+ return max(int(round(value * scale)), minimum)
+ except Exception:
+ return minimum
+
+ # 文章总览数据块字体
+ overview_summary_font = _scaled(cfg.page.font_size_base, db.overview_text_scale, db.min_overview_font)
+ overview_badge_font = _scaled(max(cfg.page.font_size_base - 2, db.min_overview_font), db.overview_text_scale, db.min_overview_font)
+ overview_kpi_value = _scaled(cfg.kpi_card.font_size_value, db.overview_kpi_scale, db.min_overview_font + 1)
+ overview_kpi_label = _scaled(cfg.kpi_card.font_size_label, db.overview_kpi_scale, db.min_overview_font)
+ overview_kpi_delta = _scaled(cfg.kpi_card.font_size_change, db.overview_kpi_scale, db.min_overview_font)
+
+ # 正文数据块字体
+ body_kpi_value = _scaled(cfg.kpi_card.font_size_value, db.body_kpi_scale, db.min_body_font + 1)
+ body_kpi_label = _scaled(cfg.kpi_card.font_size_label, db.body_kpi_scale, db.min_body_font)
+ body_kpi_delta = _scaled(cfg.kpi_card.font_size_change, db.body_kpi_scale, db.min_body_font)
+ body_callout_title = _scaled(cfg.callout.font_size_title, db.body_text_scale, db.min_body_font + 1)
+ body_callout_content = _scaled(cfg.callout.font_size_content, db.body_text_scale, db.min_body_font)
+ body_table_header = _scaled(cfg.table.font_size_header, db.body_text_scale, db.min_body_font)
+ body_table_body = _scaled(cfg.table.font_size_body, db.body_text_scale, db.min_body_font)
+ body_chart_title = _scaled(cfg.chart.font_size_title, db.body_text_scale, db.min_body_font + 1)
+ body_badge_font = _scaled(max(cfg.page.font_size_base - 2, db.min_body_font), db.body_text_scale, db.min_body_font)
css = f"""
/* PDF布局优化样式 - 由PDFLayoutOptimizer自动生成 */
@@ -734,12 +832,100 @@ p {{
margin-bottom: {cfg.page.section_spacing}px;
}}
-/* KPI卡片优化 - 防止溢出 */
+/* KPI卡片优化 - WeasyPrint不支持CSS Grid,使用Flex实现等宽排布 */
.kpi-grid {{
- display: grid;
- grid-template-columns: repeat({cfg.grid.columns}, 1fr);
+ display: flex !important;
+ flex-wrap: wrap;
gap: {cfg.grid.gap}px;
margin: 20px 0;
+ align-items: stretch;
+ page-break-inside: avoid !important;
+ break-inside: avoid !important;
+ page-break-after: avoid !important;
+ page-break-before: avoid !important;
+ break-before: avoid !important;
+ break-after: avoid !important;
+}}
+
+.kpi-grid .kpi-card {{
+ box-sizing: border-box;
+ flex: 0 1 calc(33.333% - {cfg.grid.gap}px) !important;
+ max-width: calc(33.333% - {cfg.grid.gap}px) !important;
+}}
+
+/* 单条/双条/三条的特殊列数 */
+.chapter .kpi-grid[data-kpi-count="1"] .kpi-card {{
+ flex-basis: 100% !important;
+ max-width: 100% !important;
+}}
+.chapter .kpi-grid[data-kpi-count="2"] .kpi-card {{
+ flex-basis: calc(50% - {cfg.grid.gap}px) !important;
+ max-width: calc(50% - {cfg.grid.gap}px) !important;
+}}
+.chapter .kpi-grid[data-kpi-count="3"] .kpi-card {{
+ flex-basis: calc(33.333% - {cfg.grid.gap}px) !important;
+ max-width: calc(33.333% - {cfg.grid.gap}px) !important;
+}}
+
+/* 四条时采用2x2排布 */
+.chapter .kpi-grid[data-kpi-count="4"] .kpi-card {{
+ flex-basis: calc(50% - {cfg.grid.gap}px) !important;
+ max-width: calc(50% - {cfg.grid.gap}px) !important;
+}}
+.chapter .kpi-grid[data-kpi-count="4"] {{
+ page-break-before: auto !important;
+ break-before: auto !important;
+ page-break-inside: avoid !important;
+ margin-top: 8px !important;
+}}
+
+/* hr 与紧随的KPI/正文保持同页,减少多余空白 */
+hr {{
+ page-break-before: avoid !important;
+ page-break-after: avoid !important;
+ break-before: avoid !important;
+ break-after: avoid !important;
+ margin: 12px 0 !important;
+}}
+
+/* 五条及以上默认三列(6个自动两行3+3) */
+.chapter .kpi-grid[data-kpi-count="5"] .kpi-card,
+.chapter .kpi-grid[data-kpi-count="6"] .kpi-card,
+.chapter .kpi-grid[data-kpi-count="7"] .kpi-card,
+.chapter .kpi-grid[data-kpi-count="8"] .kpi-card,
+.chapter .kpi-grid[data-kpi-count="9"] .kpi-card,
+.chapter .kpi-grid[data-kpi-count="10"] .kpi-card,
+.chapter .kpi-grid[data-kpi-count="11"] .kpi-card,
+.chapter .kpi-grid[data-kpi-count="12"] .kpi-card,
+.chapter .kpi-grid[data-kpi-count="13"] .kpi-card,
+.chapter .kpi-grid[data-kpi-count="14"] .kpi-card,
+.chapter .kpi-grid[data-kpi-count="15"] .kpi-card,
+.chapter .kpi-grid[data-kpi-count="16"] .kpi-card {{
+ flex-basis: calc(33.333% - {cfg.grid.gap}px) !important;
+ max-width: calc(33.333% - {cfg.grid.gap}px) !important;
+}}
+
+/* 5个时最后两张拉宽为两列 */
+.chapter .kpi-grid[data-kpi-count="5"] .kpi-card:nth-last-child(-n+2) {{
+ flex-basis: calc(50% - {cfg.grid.gap}px) !important;
+ max-width: calc(50% - {cfg.grid.gap}px) !important;
+}}
+
+/* 余数为2时,最后两张平分全宽 */
+.chapter .kpi-grid[data-kpi-count="8"] .kpi-card:nth-last-child(-n+2),
+.chapter .kpi-grid[data-kpi-count="11"] .kpi-card:nth-last-child(-n+2),
+.chapter .kpi-grid[data-kpi-count="14"] .kpi-card:nth-last-child(-n+2) {{
+ flex-basis: calc(50% - {cfg.grid.gap}px) !important;
+ max-width: calc(50% - {cfg.grid.gap}px) !important;
+}}
+
+/* 余数为1时,最后一张占满全宽 */
+.chapter .kpi-grid[data-kpi-count="7"] .kpi-card:last-child,
+.chapter .kpi-grid[data-kpi-count="10"] .kpi-card:last-child,
+.chapter .kpi-grid[data-kpi-count="13"] .kpi-card:last-child,
+.chapter .kpi-grid[data-kpi-count="16"] .kpi-card:last-child {{
+ flex-basis: 100% !important;
+ max-width: 100% !important;
}}
.kpi-card {{
@@ -747,35 +933,39 @@ p {{
min-height: {cfg.kpi_card.min_height}px;
break-inside: avoid;
page-break-inside: avoid;
- /* 防止溢出的关键设置 */
- overflow: hidden;
box-sizing: border-box;
max-width: 100%;
+ height: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
}}
-.kpi-card .value {{
- font-size: {cfg.kpi_card.font_size_value}px !important;
- line-height: 1.2;
- /* 强制换行和溢出控制 */
+.kpi-card .kpi-value {{
+ font-size: {body_kpi_value}px !important;
+ line-height: 1.25;
word-break: break-word;
overflow-wrap: break-word;
hyphens: auto;
max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
-}}
-
-.kpi-card .label {{
- font-size: {cfg.kpi_card.font_size_label}px !important;
- /* 防止标签溢出 */
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 4px 6px;
+.kpi-card .kpi-label {{
+ font-size: {body_kpi_label}px !important;
word-break: break-word;
overflow-wrap: break-word;
max-width: 100%;
+ line-height: 1.35;
}}
-.kpi-card .change {{
- font-size: {cfg.kpi_card.font_size_change}px !important;
+.kpi-card .change,
+.kpi-card .delta {{
+ font-size: {body_kpi_delta}px !important;
word-break: break-word;
+ overflow-wrap: break-word;
+ line-height: 1.3;
}}
/* 提示框优化 - 防止溢出 */
@@ -783,6 +973,7 @@ p {{
padding: {cfg.callout.padding}px !important;
margin: 20px 0;
line-height: {cfg.callout.line_height};
+ font-size: {body_callout_content}px !important;
break-inside: avoid;
page-break-inside: avoid;
/* 防止溢出 */
@@ -792,19 +983,31 @@ p {{
}}
.callout-title {{
- font-size: {cfg.callout.font_size_title}px !important;
+ font-size: {body_callout_title}px !important;
margin-bottom: 10px;
word-break: break-word;
line-height: 1.4;
}}
.callout-content {{
- font-size: {cfg.callout.font_size_content}px !important;
+ font-size: {body_callout_content}px !important;
word-break: break-word;
overflow-wrap: break-word;
line-height: {cfg.callout.line_height};
}}
+.callout strong {{
+ font-size: {body_callout_title}px !important;
+}}
+
+.callout p,
+.callout li,
+.callout table,
+.callout td,
+.callout th {{
+ font-size: {body_callout_content}px !important;
+}}
+
/* 确保 callout 内部最后一个元素不会溢出底部 */
.callout > *:last-child,
.callout > *:last-child > *:last-child {{
@@ -824,7 +1027,7 @@ table {{
}}
th {{
- font-size: {cfg.table.font_size_header}px !important;
+ font-size: {body_table_header}px !important;
padding: {cfg.table.cell_padding}px !important;
/* 表头文字控制 */
word-break: break-word;
@@ -834,7 +1037,7 @@ th {{
}}
td {{
- font-size: {cfg.table.font_size_body}px !important;
+ font-size: {body_table_body}px !important;
padding: {cfg.table.cell_padding}px !important;
max-width: {cfg.table.max_cell_width}px;
/* 强制换行,防止溢出 */
@@ -859,7 +1062,7 @@ td {{
}}
.chart-title {{
- font-size: {cfg.chart.font_size_title}px !important;
+ font-size: {body_chart_title}px !important;
word-break: break-word;
}}
@@ -928,19 +1131,26 @@ td {{
.hero-side {{
flex: 3; /* 右侧占30% */
min-width: 0;
+ min-height: 0;
display: flex;
- flex-direction: column;
+ flex-wrap: wrap;
gap: {max(cfg.grid.gap - 2, 10)}px;
overflow: hidden;
box-sizing: border-box;
+ width: 100%;
}}
/* Hero区域的KPI卡片 - 横向拉长,每行显示一个内容 */
.hero-kpi {{
+ background: #ffffff;
+ border-radius: 16px !important;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.08);
+ flex: 0 1 calc(50% - {max(cfg.grid.gap - 2, 10)}px);
+ max-width: calc(50% - {max(cfg.grid.gap - 2, 10)}px);
padding: 12px 18px !important; /* 增加横向padding */
overflow: hidden;
box-sizing: border-box;
- max-width: 100%;
min-height: 85px; /* 增加高度以容纳三行 */
display: flex;
flex-direction: column;
@@ -948,7 +1158,7 @@ td {{
}}
.hero-kpi .label {{
- font-size: {max(cfg.kpi_card.font_size_label - 3, 9)}px !important; /* 减小标签字号 */
+ font-size: {overview_kpi_label}px !important; /* 适度减小标签字号 */
word-break: break-word;
max-width: 100%;
line-height: 1.2;
@@ -959,7 +1169,7 @@ td {{
}}
.hero-kpi .value {{
- font-size: {max(cfg.kpi_card.font_size_value - 12, 14)}px !important; /* 减小数值字号 */
+ font-size: {overview_kpi_value}px !important; /* 适度减小数值字号 */
word-break: break-word;
overflow-wrap: break-word;
max-width: 100%;
@@ -972,7 +1182,7 @@ td {{
}}
.hero-kpi .delta {{
- font-size: {max(cfg.kpi_card.font_size_change - 3, 9)}px !important; /* 减小变化值字号 */
+ font-size: {overview_kpi_delta}px !important; /* 适度减小变化值字号 */
word-break: break-word;
margin-top: 3px;
display: block; /* 独占一行 */
@@ -984,7 +1194,7 @@ td {{
/* Hero summary文本 */
.hero-summary {{
- font-size: {cfg.page.font_size_base}px !important;
+ font-size: {overview_summary_font}px !important;
line-height: 1.65;
margin-top: 0;
margin-bottom: 18px; /* 增加底部边距,与badges保持一致 */
@@ -1014,7 +1224,7 @@ td {{
/* hero highlights中的badge - 拉长加宽的椭圆形背景,与上方文本对齐 */
.hero-highlights .badge {{
- font-size: {max(cfg.callout.font_size_content - 3, 10)}px !important;
+ font-size: {overview_badge_font}px !important;
padding: 10px 20px !important; /* 增加padding,更好的视觉效果 */
max-width: 100%;
width: 98%; /* 占满宽度,与summary文本对齐 */
@@ -1105,7 +1315,7 @@ main > .chapter:first-of-type {{
white-space: normal;
/* 限制badge的最大尺寸 */
padding: 4px 12px !important;
- font-size: {max(cfg.page.font_size_base - 2, 12)}px !important;
+ font-size: {body_badge_font}px !important;
line-height: 1.4 !important;
/* 防止badge异常过大 */
word-break: break-word;
@@ -1147,4 +1357,5 @@ __all__ = [
'TableLayout',
'ChartLayout',
'GridLayout',
+ 'DataBlockLayout',
]