const REPORT_ENHANCED_STYLE = `:root { --primary: #1e3a5f; --primary-light: #2d5a8a; --accent: #e8b339; --accent2: #3b82f6; --success: #10b981; --danger: #ef4444; --warning: #f59e0b; --bg: #f0f4f8; --card-bg: #ffffff; --text: #1e293b; --text-secondary: #64748b; --border: #e2e8f0; --shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1); --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1); --radius: 16px; --radius-sm: 10px; } *{box-sizing:border-box;margin:0;padding:0} body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; } .wrap {max-width:1200px;margin:auto;padding:24px 16px 48px} .hero { background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%); border-radius: var(--radius); padding: 40px 32px; margin-bottom: 24px; color: white; position: relative; overflow: hidden; } .hero::before { content: ''; position: absolute; top: -50%; right: -20%; width: 400px; height: 400px; background: rgba(255,255,255,0.05); border-radius: 50%; } .hero h1 { font-size: 28px; font-weight: 700; margin-bottom: 8px; position: relative; } .hero .subtitle { font-size: 15px; opacity: 0.85; margin-bottom: 24px; position: relative; } .hero .source { font-size: 13px; opacity: 0.7; position: relative; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; } .stat-card { background: var(--card-bg); border-radius: var(--radius); padding: 24px; box-shadow: var(--shadow); border: 1px solid var(--border); position: relative; overflow: hidden; transition: transform 0.2s, box-shadow 0.2s; } .stat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); } .stat-card .icon { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; margin-bottom: 12px; } .stat-card.primary .icon {background: #dbeafe;} .stat-card.success .icon {background: #d1fae5;} .stat-card.warning .icon {background: #fef3c7;} .stat-card.info .icon {background: #e0e7ff;} .stat-card .label { font-size: 13px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; } .stat-card .value { font-size: 28px; font-weight: 700; color: var(--primary); } .stat-card .trend { font-size: 12px; margin-top: 6px; display: flex; align-items: center; gap: 4px; } .trend.up {color: var(--success);} .trend.down {color: var(--danger);} .section { background: var(--card-bg); border-radius: var(--radius); padding: 28px; margin-bottom: 20px; box-shadow: var(--shadow); border: 1px solid var(--border); } .section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; flex-wrap: wrap; gap: 12px; } .section-title { font-size: 18px; font-weight: 700; color: var(--primary); display: flex; align-items: center; gap: 10px; } .section-title .badge { background: var(--accent); color: var(--primary); font-size: 11px; padding: 3px 10px; border-radius: 20px; font-weight: 600; } .section-subtitle { font-size: 13px; color: var(--text-secondary); } .chart-container { background: #fafbfc; border-radius: var(--radius-sm); padding: 20px; margin-bottom: 20px; border: 1px solid var(--border); } .chart-container img { width: 100%; height: auto; border-radius: 8px; } .chart-bar { display: flex; align-items: flex-end; gap: 8px; height: 200px; padding: 16px 8px 32px; position: relative; } .chart-bar::after { content: ''; position: absolute; bottom: 32px; left: 8px; right: 8px; height: 1px; background: var(--border); } .bar-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 0; } .bar-fill { width: 100%; max-width: 48px; border-radius: 6px 6px 0 0; transition: height 0.8s ease-out; position: relative; } .bar-fill:hover { filter: brightness(1.1); } .bar-fill .tooltip { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: var(--primary); color: white; font-size: 11px; padding: 4px 8px; border-radius: 6px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; } .bar-fill:hover .tooltip { opacity: 1; } .bar-label { font-size: 11px; color: var(--text-secondary); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } .table-wrap {overflow:auto;margin-top:12px} table {width:100%;border-collapse:separate;border-spacing:0;font-size:14px} thead th { background: var(--primary); color: white; font-weight: 600; padding: 12px 14px; text-align: right; white-space: nowrap; font-size: 13px; } thead th:first-child {text-align:left;border-radius:10px 0 0 0} thead th:last-child {border-radius:0 10px 0 0} tbody td { padding: 12px 14px; border-bottom: 1px solid var(--border); text-align: right; white-space: nowrap; } tbody td:first-child {text-align:left;font-weight:500} tbody tr:hover td {background:#f8fafc} tbody tr:last-child td:first-child {border-radius:0 0 0 10px} tbody tr:last-child td:last-child {border-radius:0 0 10px 0} .rank-1 {color: var(--accent); font-weight: 700;} .rank-2 {color: var(--text-secondary); font-weight: 600;} .rank-3 {color: #b45309; font-weight: 600;} .cell-bar { display: flex; align-items: center; gap: 8px; } .cell-bar .track { flex: 1; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; min-width: 60px; } .cell-bar .fill { height: 100%; border-radius: 3px; transition: width 0.8s ease-out; } .insights { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-top: 20px; } .insight-card { background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); border-radius: var(--radius-sm); padding: 20px; border-left: 4px solid var(--accent2); } .insight-card h4 { font-size: 14px; color: var(--primary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; } .insight-card p { font-size: 13px; color: var(--text-secondary); line-height: 1.7; } footer { text-align: center; padding: 32px 16px; color: var(--text-secondary); font-size: 13px; } @media (max-width: 640px) { .hero {padding: 28px 20px} .hero h1 {font-size: 22px} .stats-grid {grid-template-columns: repeat(2, 1fr)} .stat-card .value {font-size: 22px} .section {padding: 20px} .chart-bar {height: 160px} } @keyframes fadeInUp { from {opacity:0;transform:translateY(20px)} to {opacity:1;transform:translateY(0)} } .stat-card, .section, .insight-card { animation: fadeInUp 0.5s ease-out both; } .stat-card:nth-child(1) {animation-delay:0.05s} .stat-card:nth-child(2) {animation-delay:0.1s} .stat-card:nth-child(3) {animation-delay:0.15s} .stat-card:nth-child(4) {animation-delay:0.2s}`; const SECTION_LAYOUT = { 区域: { emoji: '🗺️', badge: (count) => `${count}大区域`, subtitle: '各区域销售额、订单量与单均表现', chartId: 'regionChart', tableBarColor(index) { return index <= 4 ? 'var(--accent2)' : 'var(--danger)'; }, chartColor(index) { if (index <= 3) return '#3b82f6'; if (index === 4) return '#f59e0b'; return '#ef4444'; }, }, 城市: { emoji: '🏙️', badge: (count) => `TOP ${count}`, subtitle: '各城市销售额排名与表现', chartId: 'cityChart', tableBarColor(index) { if (index <= 5) return 'var(--accent2)'; if (index <= 9) return 'var(--warning)'; return 'var(--danger)'; }, chartColor(index) { if (index <= 5) return '#3b82f6'; if (index <= 9) return '#f59e0b'; return '#ef4444'; }, }, 产品: { emoji: '🍜', badge: (count) => `${count}种产品`, subtitle: '各产品销售额、销量与单均表现', chartId: 'productChart', tableBarColor(index) { if (index <= 5) return 'var(--accent2)'; if (index <= 8) return 'var(--warning)'; return 'var(--danger)'; }, chartColor(index) { if (index <= 5) return '#3b82f6'; if (index <= 8) return '#f59e0b'; return '#ef4444'; }, }, }; function escapeXml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function safeJsonForScript(value) { return JSON.stringify(value).replace(/= 100 && Math.abs(rounded - Math.round(rounded)) <= 0.15) { return `${Math.round(rounded)}万`; } const text = rounded.toFixed(1); return text.endsWith('.0') ? `${Math.round(rounded)}万` : `${text}万`; } function formatWanPerOrder(value) { return formatWan(value); } function sectionLayout(dimension) { return SECTION_LAYOUT[dimension] ?? { emoji: '📊', badge: (count) => `${count} 项`, subtitle: `各${dimension}表现`, chartId: `chart${dimension}`, tableBarColor: () => 'var(--accent2)', chartColor: () => '#3b82f6', }; } function formatBarWidth(widthPct) { const raw = Number(widthPct) || 0; const nearest = Math.round(raw); if (Math.abs(raw - nearest) < 0.055) { return String(nearest); } const rounded = Math.round(raw * 10) / 10; return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1).replace(/\.0$/, ''); } function shareCell(value, topValue, grandTotal, barColor) { const sharePct = reportSharePercent(value, grandTotal); const widthPct = topValue ? (Number(value) / Number(topValue)) * 100 : 0; const widthText = formatBarWidth(widthPct); const shareText = sharePct.toFixed(1); return `
`; } function rankMedalLabel(index, group) { if (index === 0) return `🥇 ${escapeXml(group)}`; if (index === 1) return `🥈 ${escapeXml(group)}`; if (index === 2) return `🥉 ${escapeXml(group)}`; return escapeXml(group); } function findSummaryMetric(model, pattern) { return (model.meta?.summaryMetrics ?? []).find((item) => pattern.test(item.name)); } function sectionByDimension(model, dimension) { return model.sections.find((section) => section.dimension === dimension) ?? null; } function buildHeroSource(model) { const parts = [ `数据来源:${model.source.path}`, `${model.source.rows}条记录`, ]; const region = sectionByDimension(model, '区域'); const city = sectionByDimension(model, '城市'); const product = sectionByDimension(model, '产品'); if (region) parts.push(`${region.items.length}大区域`); if (city) parts.push(`${city.items.length}个城市`); if (product) parts.push(`${product.items.length}种产品`); return parts.join(' · '); } function buildHeroSubtitle(model) { const dimensions = model.sections.map((section) => section.dimension).join(' · '); const period = model.meta?.periodLabel; if (period && dimensions) return `${period} · ${dimensions} 三维深度分析`; if (dimensions) return `${dimensions} 多维深度分析`; return 'Excel 数据分析报告'; } function buildStatCards(model) { const cards = []; const metricLabel = metricShortLabel(model.fields.metric); const metricPrefix = metricUsesCurrencyLabel(model.fields.metric) ? '¥' : ''; cards.push({ tone: 'primary', icon: '💰', label: `总${metricLabel}`, value: `${metricPrefix}${formatReportNumber(model.totals.metric)}`, trend: model.sections.some((section) => section.dimension === '区域') ? `↑ 覆盖${sectionByDimension(model, '区域')?.items.length ?? model.sections.length}大区域` : `↑ 覆盖${model.sections.length}个维度`, }); if (model.fields.quantityMetric) { const unit = metricUnit(model.fields.quantityMetric); const avgPerOrder = model.totals.orders ? model.totals.quantity / model.totals.orders : null; cards.push({ tone: 'success', icon: '📦', label: metricShortLabel(model.fields.quantityMetric), value: unit ? `${formatReportInteger(model.totals.quantity)} ${unit}` : formatReportInteger(model.totals.quantity), trend: avgPerOrder == null ? '↑ 已汇总' : `↑ 平均${formatReportInteger(avgPerOrder)}${unit ? `${unit}/单` : '/单'}`, }); } const dealerMetric = findSummaryMetric(model, /经销商/); if (dealerMetric) { const citySection = sectionByDimension(model, '城市'); const avgPerCity = citySection?.items.length ? dealerMetric.total / citySection.items.length : null; cards.push({ tone: 'warning', icon: '🏪', label: metricShortLabel(dealerMetric.name), value: `${formatReportInteger(dealerMetric.total)} 家`, trend: avgPerCity == null ? '↑ 已汇总' : `↑ 平均${Math.round(avgPerCity)}家/城市`, }); } else if (model.fields.profitMetric) { cards.push({ tone: 'warning', icon: '📈', label: metricShortLabel(model.fields.profitMetric), value: `${metricPrefix}${formatReportNumber(model.totals.profit)}`, trend: model.totals.margin == null ? '↑ 已汇总' : `↑ 比率 ${(model.totals.margin * 100).toFixed(1)}%`, }); } const citySection = sectionByDimension(model, '城市'); if (citySection) { cards.push({ tone: 'info', icon: '📍', label: '覆盖城市', value: `${citySection.items.length} 个`, trend: '↑ 全国布局', }); } else { cards.push({ tone: 'info', icon: '📍', label: model.fields.orderId ? '唯一订单数' : '记录数', value: `${formatReportInteger(model.totals.orders)} 个`, trend: `↑ ${model.source.rows} 行源数据`, }); } return cards.slice(0, 4).map((card) => ( `${item.text}