PDF Enhancement Generation
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user