PDF Enhancement Generation

This commit is contained in:
马一丁
2025-11-25 01:41:30 +08:00
parent 8505a8aafb
commit e9b7a91722
3 changed files with 322 additions and 50 deletions

View File

@@ -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)

View File

@@ -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'<span class="delta {delta_tone}">{self._escape_html(delta)}</span>' if delta else ""
@@ -1356,7 +1357,8 @@ class HTMLRenderer:
{delta_html}
</div>
"""
return f'<div class="kpi-grid">{cards}</div>'
count_attr = f' data-kpi-count="{len(items)}"' if items else ""
return f'<div class="kpi-grid"{count_attr}>{cards}</div>'
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'

View File

@@ -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',
]