瀏覽代碼

first save

kyle 1 周之前
當前提交
00bc71c256

+ 179 - 0
generate-data-report-ppt/SKILL.md

@@ -0,0 +1,179 @@
+---
+name: generate-data-report-ppt
+description: >
+  基于 Excel 业务明细数据,自动生成数据日报、周报、月报 PPT。
+  图表使用原生 python-pptx 可编辑 Chart 对象(非 matplotlib PNG 插入)。
+  当用户请求"生成日报/周报/月报"、"创建数据报告 PPT"、"输出业务报告 PPT"
+  或任何涉及周期性数据报告的 PowerPoint 格式需求时触发此技能。
+---
+
+# 生成数据报告 PPT
+
+自动生成周期性业务数据报告(日报 / 周报 / 月报)为原生可编辑 PowerPoint 文件。
+
+## 工作流程
+
+1. **接收输入**:数据文件路径、报告类型、日期/周期参数、可选部门/来源名称。
+2. **加载数据**:`scripts/data_loader.py` 按日期范围读取 Excel 工作表,清洗并校验数据。
+3. **计算指标**:`scripts/metrics_calculator.py` 根据报告类型计算 KPI、分布、趋势、告警、深度洞察。
+4. **生成洞察**:`scripts/deep_insights.py` 生成结构化深度分析文本(标题 + 正文段落)。
+5. **构建 PPT**:`scripts/ppt_builder.py` 从模板复制幻灯片,替换占位符,动态绘制导航标签,
+   通过 `scripts/chart_factory.py` 插入原生图表、KPI 卡片、告警卡片、洞察文本块。
+6. **输出**:保存 `.pptx` 文件。所有图表和表格均可在 PowerPoint 中编辑(右键 → 编辑数据)。
+
+## 目录结构
+
+```
+generate-data-report-ppt/
+├── SKILL.md
+├── scripts/
+│   ├── data_loader.py          # Excel 加载与数据清洗
+│   ├── metrics_calculator.py   # KPI 计算引擎(含日报/周报/月报指标)
+│   ├── deep_insights.py        # 结构化深度洞察生成(周报/月报各页面)
+│   ├── chart_factory.py        # 原生可编辑图表创建
+│   └── ppt_builder.py          # PPT 组装编排器
+├── references/
+│   ├── data-schema.md          # Excel 字段映射与校验规则
+│   ├── report-structures.md    # 日报/周报/月报页面结构
+│   ├── chart-specs.md          # 原生图表类型、配色、数据绑定
+│   └── visual-style-guide.md   # 布局、字体、配色方案
+└── assets/
+    ├── report-master.pptx      # 日报模板(封面、内容页、目录、尾页)
+    ├── weekly-master.pptx      # 周报模板
+    └── monthly-master.pptx     # 月报模板
+```
+
+## 报告类型
+
+### 日报
+- **结构**:封面 → 核心指标概览 → 近10天趋势 → 订单状态分布 → 负责人分布 → 目的国家 TOP8 → 异常告警 → 今日要点
+- **分析维度**:与昨日对比
+- **页数**:8
+
+### 周报
+- **结构**:封面 → 周汇总 → 7日趋势 → 环比分析 → 区域分布 → 国家排行 → 团队追踪 → 问题与建议 → 下周计划
+- **分析维度**:周环比(WoW)、7日移动平均
+- **页数**:9
+- **导航标签**:周汇总 / 趋势图 / 环比分析 / 区域排行 / 问题建议 / 下周计划
+
+### 月报
+- **结构**:封面 → 目录 → 月度总览 → 订单状态漏斗 → 区域分布 → TOP10 目的国 → 30日追踪趋势 → 团队绩效 → 支持需求分析 → 下月规划 → 尾页
+- **分析维度**:环比(MoM)、同比(YoY)、日均值、结构占比
+- **页数**:11
+- **导航标签**:月度总览 / 订单状态 / 区域趋势 / 团队展望
+
+## 模板使用
+
+`assets/` 下包含三种报告模板:
+
+| 报告类型 | 模板文件 | 包含幻灯片 |
+|---------|---------|-----------|
+| 日报 | `report-master.pptx` | 封面 / 内容页 / 目录页 / 尾页 |
+| 周报 | `weekly-master.pptx` | 封面 / 内容页 / 目录页 / 尾页 |
+| 月报 | `monthly-master.pptx` | 封面 / 内容页 / 目录页 / 尾页 |
+
+**复制机制**:`ppt_builder._duplicate_slide(prs, source_slide)` 深度复制模板幻灯片到输出文稿。
+
+**导航标签**:由 `ppt_builder._add_nav_tabs()` 在内容页上动态绘制,不内嵌在模板中。
+
+## 占位符替换
+
+所有模板形状使用 `{placeholder}` 语法。脚本遍历 `slide.shapes` 匹配段落文本进行替换。
+
+| 占位符 | 出现位置 | 替换内容 |
+|--------|---------|---------|
+| `{report_title}` | 封面、内容页眉、尾页 | 如"海外订单数据日报" |
+| `{report_type}` | 封面副标题 | 如"数据日报" |
+| `{date}` | 封面、页眉、尾页 | 报告日期或周期 |
+| `{department}` | 封面、尾页 | 如"海外事业部" |
+| `{period}` | 封面、底部来源条 | 数据周期描述 |
+| `{gen_time}` | 封面 | 报告生成时间 |
+| `{page_title}` | 内容页 | 当前页面标题 |
+| `{page_num}` | 右下角 | 如"3 / 8" |
+| `{source}` | 底部来源条 | 数据来源 |
+| `{kpiN_label}` / `{kpiN_value}` | 封面/尾页卡片 | 第N个指标的标签和数值 |
+| `{chapterN_title}` / `{chapterN_desc}` | 目录网格 | 第N章标题和描述 |
+
+## 图表插入规则
+
+**严格使用原生图表**,禁止生成 matplotlib PNG 图片。
+
+| 图表类型 | XL_CHART_TYPE | 使用场景 |
+|---------|---------------|---------|
+| 簇状柱形图 | COLUMN_CLUSTERED | 区域分布、团队追踪、支持需求分布 |
+| 簇状条形图(横向) | BAR_CLUSTERED | 国家排名、负责人排名、状态漏斗 |
+| 折线图(带标记) | LINE_MARKERS | 多日趋势(10天/7天/30天) |
+| 环形图 | DOUGHNUT | 状态占比、区域占比 |
+| 饼图 | PIE | 状态占比、区域占比(替代场景) |
+| 表格 | TABLE | 明细列表、TOP列表、状态变化、超期订单 |
+
+调用 `chart_factory.add_*_chart()` 传入数据数组。图表数据嵌入 PPT 内部 Excel 工作簿,用户可直接编辑。
+
+## 数据输入要求
+
+Excel 文件按自然日分 Sheet,工作表命名:`YYYY年MM月DD日`(如 `2026年04月10日`)。
+
+**必填字段**:`目的国家`、`合同号`、`订单总数量`、`负责人`、`当前状态`、`拟定合同时间`
+
+**推荐字段**:`今日进度更新`、`是否更新`、`支持需求`、`4月交付`、`5月预测`
+
+完整字段映射、状态枚举(A-F)及校验规则见 `references/data-schema.md`。
+
+## 配色方案
+
+| 角色 | 色值 | 用途 |
+|------|------|------|
+| 主色 | `#1E3A5F` | 页眉标题、导航标签、强调色、顶部蓝线 |
+| 辅色 | `#5B9BD5` | 图表主系列、CONTENTS 标签 |
+| 深色背景 | `#1F3A5C` | 封面左侧块 |
+| 增长色 | `#10B981` | 上涨指标、正面变化 |
+| 下跌色 | `#EF4444` | 下跌指标、负面变化、严重告警 |
+| 警告色 | `#ED7D31` | 中度告警、关注提示 |
+| 卡片背景 | `#E7F0F7` | KPI 卡片背景 |
+| 深灰文字 | `#333333` | 正文、主标题 |
+| 中灰文字 | `#666666` | 副标题、次要信息 |
+
+## 核心功能模块
+
+### 指标计算(metrics_calculator.py)
+- **日报指标**:在跟订单数、订单总量、今日更新、已发运、支持需求、下月预测、单均台数、状态分布、负责人分布、国家 TOP8、超期订单(A阶段>30天)、告警列表
+- **周报指标**:周订单量、周车辆数、日均订单、单均台数、7日趋势、状态环比(WoW)、区域分布(含各国 TOP3)、国家排行(含环比变化)、团队绩效(人均产出)、支持需求分类、问题识别、下周目标(G1-G4自动生成)
+- **月报指标**:月度合同数、月度车辆数、新签合同、已发运、覆盖国家数、支持需求占比、日均订单、状态漏斗(含阶段分析:前期/中期/后期)、区域分布(含各国 TOP3)、TOP10 国家(含环比变化)、30日趋势(含上中下旬均值、峰值日期)、团队绩效(人均订单/人均台数)、超期订单、下月目标(5项自动生成)、风险列表
+
+### 深度洞察(deep_insights.py)
+为周报和月报各页面生成结构化洞察文本,每条洞察包含标题和正文:
+- **周报**:周内节奏分析、周环比趋势偏移、月度进度推演、关键驱动因素、区域引擎识别、结构健康度、转化效率、瓶颈诊断、库存资金占用、发运端效率、漏斗健康度、区域战略矩阵、国家组合健康度、团队人均产出、问题根因分类、目标拆解、风险对冲等
+- **月报**:月度节奏、目标达成率、季节性同比、年度进度、漏斗结构诊断、区域投入 ROI、国家增速梯队、团队均衡性、支持需求趋势、下月里程碑等
+
+### PPT 组件(ppt_builder.py)
+- **KPI 卡片**:3×2 网格,支持数值、单位、变化徽章、情感标签(自动着色)
+- **告警卡片**:1-3 个横向排列,支持严重/警告/关注三级颜色
+- **问题卡片**:纵向堆叠,含严重度、标题、详情、建议措施
+- **目标卡片**:2×2 网格,含图标、目标编号、标题、详情
+- **结构化洞察文本块**:多段落洞察,自适应字号压缩以适配高度,带 emoji 前缀
+- **页脚**:自动添加数据来源条和页码
+
+## 执行示例
+
+```python
+from scripts.ppt_builder import build_daily_report, build_weekly_report, build_monthly_report
+from datetime import datetime
+
+# 日报
+build_daily_report('data.xlsx', datetime(2026, 4, 10), 'daily_20260410.pptx')
+
+# 周报(2026年第14周)
+build_weekly_report('data.xlsx', 2026, 14, 'weekly_w14.pptx')
+
+# 月报(2026年4月)
+build_monthly_report('data.xlsx', 2026, 4, 'monthly_202604.pptx')
+```
+
+## 扩展技能
+
+添加新报告类型(如季报):
+1. 在 `references/report-structures.md` 中添加页面结构定义
+2. 在 `scripts/metrics_calculator.py` 中添加指标计算函数
+3. 在 `scripts/deep_insights.py` 中添加洞察生成函数
+4. 在 `scripts/ppt_builder.py` 中添加构建函数
+5. 若内容页布局通用,无需修改模板文件

二進制
generate-data-report-ppt/assets/monthly-master.pptx


二進制
generate-data-report-ppt/assets/report-master.pptx


二進制
generate-data-report-ppt/assets/weekly-master.pptx


+ 78 - 0
generate-data-report-ppt/references/chart-specs.md

@@ -0,0 +1,78 @@
+# 图表规范(原生可编辑)
+
+所有图表必须使用 `python-pptx` 原生 `Chart` 对象创建,数据写入图表内嵌 Excel 工作簿,确保用户在 PowerPoint 中可右键 → 编辑数据。
+
+## 支持的图表类型
+
+| 图表类型 | XL_CHART_TYPE | 值 | 使用场景 |
+|---------|---------------|-----|---------|
+| 簇状柱形图 | COLUMN_CLUSTERED | 51 | 区域分布对比、团队追踪 |
+| 簇状条形图 | BAR_CLUSTERED | 57 | 国家排名、负责人排名、状态漏斗(横向) |
+| 折线图(带标记) | LINE_MARKERS | 65 | 多日趋势、30日追踪、7日走势 |
+| 环形图 | DOUGHNUT | -4120 | 状态分布、区域占比 |
+| 表格 | TABLE | 19 | 明细数据、TOP列表、状态变化 |
+
+## 配色映射
+
+| 用途 | 色值 | 说明 |
+|------|------|------|
+| 系列1主色 | `#5B9BD5` | 蓝色,主要数据系列 |
+| 系列2对比 | `#ED7D31` | 橙色,对比/参考系列 |
+| 系列3辅助 | `#A5A5A5` | 灰色,辅助线或第三系列 |
+| 正增长 | `#70AD47` | 绿色,用于涨幅标记 |
+| 负增长 | `#C5504B` | 红色,用于跌幅标记 |
+| 警告 | `#ED7D31` | 橙色,异常告警 |
+
+## 通用图表样式
+
+1. **标题**:图表本身不设置标题(页面顶部已有文本框标题)
+2. **图例**:单系列时隐藏;多系列时放置于底部
+3. **数据标签**:显示数值,字体 Arial 10pt,颜色 `#333333`
+4. **网格线**:仅保留水平主网格线,颜色 `#D9D9D9`,宽度 0.75pt
+5. **坐标轴**:无填充、无线条;标签字体 10pt,颜色 `#666666`
+6. **绘图区**:无填充、无边框
+7. **图表区**:无填充、无边框
+
+## 图表数据绑定
+
+```python
+from pptx.chart.data import ChartData
+from pptx.enum.chart import XL_CHART_TYPE
+
+chart_data = ChartData()
+chart_data.categories = ['亚洲', '非洲', '拉美', '中东', '欧洲']
+chart_data.add_series('订单量', (2160, 1840, 1743, 878, 247))
+
+chart = slide.shapes.add_chart(
+    XL_CHART_TYPE.COLUMN_CLUSTERED,
+    left, top, width, height,
+    chart_data
+).chart
+```
+
+数据自动写入 PPT 内嵌的 Excel 工作簿,用户可在 PowerPoint 中直接编辑。
+
+## 特殊配置
+
+### 条形图逆序(最大值在顶部)
+
+```python
+chart = slide.shapes.add_chart(XL_CHART_TYPE.BAR_CLUSTERED, ...).chart
+chart.category_axis.reverse_order = True
+```
+
+### 环形图中心空白
+
+```python
+chart = slide.shapes.add_chart(XL_CHART_TYPE.DOUGHNUT, ...).chart
+chart.plots[0].hole_size = 0.5  # 50% 中心空白
+```
+
+### 折线图标记样式
+
+```python
+chart = slide.shapes.add_chart(XL_CHART_TYPE.LINE_MARKERS, ...).chart
+series = chart.series[0]
+series.marker.size = 7
+series.marker.style = XL_MARKER_STYLE.CIRCLE
+```

+ 61 - 0
generate-data-report-ppt/references/data-schema.md

@@ -0,0 +1,61 @@
+# 数据源 Schema
+
+数据输入为 Excel 文件,每个工作表(Sheet)代表一个自然日的订单明细数据。
+
+## 工作表命名规则
+
+- 日报数据源:`YYYY年MM月DD日`(如 `2026年04月10日`)
+- 脚本通过日期字符串匹配对应工作表
+
+## 字段映射
+
+| Excel 列名 | 内部字段名 | 数据类型 | 说明 |
+|-----------|-----------|---------|------|
+| 序号 | `seq` | int | 行序号 |
+| 目的国家 | `country` | str | 订单目的国家/地区 |
+| 合同号 | `contract_no` | str | 唯一合同编号 |
+| 用户名称/公司 | `customer` | str | 客户名称 |
+| 意向车型及数量 | `product_info` | str | 车型及台数描述 |
+| 订单总数量 | `order_qty` | int | 该合同的车辆总台数 |
+| 负责人 | `owner` | str | 跟单负责人姓名 |
+| 当前状态 | `status` | str | 订单阶段,见下方状态枚举 |
+| 拟定合同时间 | `contract_date` | datetime | 合同拟定日期 |
+| 跟单天数 | `tracking_days` | int | 从合同拟定到当前日期的天数 |
+| 定金支付时间 | `deposit_date` | datetime | 定金支付日期 |
+| 订金认领时间 | `deposit_claim_date` | datetime | 订金认领日期 |
+| 订单生成时间 | `order_gen_date` | datetime | 订单在系统生成日期 |
+| 价格评审时间 | `price_review_date` | datetime | 价格评审完成日期 |
+| 合同评审时间 | `contract_review_date` | datetime | 合同评审完成日期 |
+| 合同提交盖章申请时间 | `seal_apply_date` | datetime | 盖章申请日期 |
+| 合同盖章时间 | `seal_date` | datetime | 合同盖章完成日期 |
+| 车辆下线入库状态 | `inventory_status` | str | 车辆生产/入库状态描述 |
+| 尾款支付时间 | `final_pay_date` | datetime | 尾款支付日期 |
+| 尾款认领时间 | `final_claim_date` | datetime | 尾款认领日期 |
+| 智慧关务信息维护 | `customs_date` | datetime | 关务信息维护日期 |
+| 许可证办理时间 | `license_date` | datetime | 进口许可证办理日期 |
+| 车辆发运时间 | `ship_date` | datetime | 实际发运日期 |
+| 预计开票时间 | `invoice_date` | datetime | 预计开票日期 |
+| 今日进度更新 | `progress_update` | str | 当日最新进度描述 |
+| 是否更新 | `is_updated` | str (是/否) | 当日是否有进度更新 |
+| 支持需求 | `support_request` | str | 需要跨部门支持的需求描述 |
+| 4月交付 | `deliver_apr` | int | 标记为4月交付的台数 |
+| 5月预测 | `forecast_may` | int | 标记为5月预测交付的台数 |
+
+## 订单状态枚举
+
+| 状态代码 | 状态名称 | 说明 |
+|---------|---------|------|
+| A | 合同拟定中 | 合同尚未盖章确认 |
+| B | 已锁定合同待付订金 | 合同已盖章,等待客户支付订金 |
+| C | 已付订金待生产 | 订金已到账,等待排产 |
+| D | 已生产待付尾款 | 车辆已生产/入库,等待尾款 |
+| E | 已付尾款待发运 | 尾款已到账,等待发运安排 |
+| F | 已发运 | 车辆已发运 |
+
+## 数据校验规则
+
+1. **必填字段**:`country`, `contract_no`, `order_qty`, `owner`, `status`, `contract_date`
+2. `order_qty` 必须为正整数
+3. `status` 必须为 A-F 中的一个
+4. `is_updated` 只能为 "是" 或 "否"
+5. 日期字段若为字符串,尝试按 `YYYY-MM-DD` 解析

+ 84 - 0
generate-data-report-ppt/references/report-structures.md

@@ -0,0 +1,84 @@
+# 报告页面结构规范
+
+三种报告类型的标准页面结构、分析维度和占位符规则。
+
+---
+
+## 日报(Daily Report)
+
+**分析维度**:与昨日对比、与上周同日对比
+
+| 页码 | 页面标题 | 内容元素 | 图表类型 |
+|------|---------|---------|---------|
+| 1 | 封面 | 标题、日期、部门、右侧KPI卡片 | 无 |
+| 2 | 今日核心指标概览 | 6个KPI卡片:在跟订单/订单总量/今日更新/下月预测/支持需求/已发运 | 无 |
+| 3 | 近N天订单趋势 | 柱状图(近10天订单量走势)+ 右侧趋势洞察文本 | COLUMN_CLUSTERED |
+| 4 | 订单状态分布 | 环形图(A-F阶段占比)+ 状态变化对比表格 | DOUGHNUT + TABLE |
+| 5 | 负责人订单分布 | 横向条形图(负责人 vs 订单数) | BAR_CLUSTERED |
+| 6 | 目的国家TOP8 | 横向条形图(国家 vs 订单量) | BAR_CLUSTERED |
+| 7 | 异常告警 | 三级告警卡片(严重/警告/关注)+ 支持需求分类统计表格 | TABLE |
+| 8 | 今日要点 | 3栏行动建议(重点推进/跨部门协调/关注订单) | 无 |
+
+**日报封面右侧KPI**:在跟订单笔数 / 订单总数量 / 今日已更新 / 支持需求数(4选3或4个)
+
+---
+
+## 周报(Weekly Report)
+
+**分析维度**:周环比(WoW)、周同比(YoY)、7日移动平均
+
+| 页码 | 页面标题 | 内容元素 | 图表类型 |
+|------|---------|---------|---------|
+| 1 | 封面 | 标题、周期(如"4月第2周")、部门、右侧3个核心数字 | 无 |
+| 2 | 周汇总 | 6个指标卡片 + 周环比 + 日均值 | 无 |
+| 3 | 7日趋势图 | 折线图(订单量7日走势)+ 右侧本周关键数据面板 | LINE_MARKERS |
+| 4 | 环比分析 | 条形图(各环节环比变化A-F)+ 右侧文字解读 | BAR_CLUSTERED |
+| 5 | 区域分布 | 柱状图(大洲维度)+ 右侧区域占比文字 | COLUMN_CLUSTERED |
+| 6 | TOP国家排行 | 横向条形图(国家TOP15)+ 右侧TOP6亮点文字 | BAR_CLUSTERED |
+| 7 | 团队追踪 | 柱状图(负责人订单数)+ 右侧增长TOP3文字 | COLUMN_CLUSTERED |
+| 8 | 问题与建议 | 3个问题卡片(严重/中度/中度),含问题描述+建议措施+影响评估 | 无 |
+| 9 | 下周计划 | G1-G4目标卡片 + 本周核心总结 | 无 |
+
+**周报导航标签**(由脚本动态绘制):周汇总 / 趋势图 / 环比分析 / 区域排行 / 问题建议 / 下周计划
+
+---
+
+## 月报(Monthly Report)
+
+**分析维度**:环比(MoM)、同比(YoY)、日均值、结构占比
+
+| 页码 | 页面标题 | 内容元素 | 图表类型 |
+|------|---------|---------|---------|
+| 1 | 封面 | 标题、月份、右侧4个核心数字(合同/车辆/国家/团队) | 无 |
+| 2 | 目录 | 4大章节索引 | 无 |
+| 3 | 月度总览 | 6个KPI卡片 + 关键发现 | 无 |
+| 4 | 订单状态漏斗 | 条形图(A-F各阶段订单数/车辆数)+ 关键发现 | BAR_CLUSTERED |
+| 5 | 区域分布 | 环形图(大洲占比)+ 表格 + 区域洞察 | DOUGHNUT + TABLE |
+| 6 | TOP10目的国 | 条形图 + 表格 + 国家洞察 | BAR_CLUSTERED + TABLE |
+| 7 | 30日追踪趋势 | 折线图(30天订单量)+ 底部4栏(上旬/中旬/下旬日均+峰值日期) | LINE_MARKERS |
+| 8 | 团队绩效 | 条形图 + 表格 + 团队洞察 | BAR_CLUSTERED + TABLE |
+| 9 | 支持需求分析 | 条形图(需求类型分布)+ 右侧优化建议 | BAR_CLUSTERED |
+| 10 | 下月规划 | 左侧工作目标 + 右侧风险提示与应对 | 无 |
+| 11 | 尾页 | 感谢页 + 底部4个核心数字 | 无 |
+
+**月报导航标签**(由脚本动态绘制):月度总览 / 订单状态 / 区域趋势 / 团队展望
+
+---
+
+## 通用占位符规则
+
+所有模板幻灯片中的文本框使用 `{placeholder}` 格式命名,脚本通过遍历 shapes 匹配文本进行替换:
+
+| 占位符 | 出现位置 | 说明 |
+|--------|---------|------|
+| `{report_title}` | 封面页眉、尾页 | 报告主标题 |
+| `{report_type}` | 封面副标题 | 日报/周报/月报 |
+| `{date}` | 封面、内容页页眉、尾页 | 报告日期或周期 |
+| `{department}` | 封面、尾页 | 报告部门 |
+| `{period}` | 封面、内容页底部 | 数据周期描述 |
+| `{gen_time}` | 封面 | 报告生成时间 |
+| `{page_title}` | 内容页 | 当前页面标题 |
+| `{source}` | 内容页底部 | 数据来源 |
+| `{page_num}` | 内容页底部 | 页码 |
+| `{kpiN_label}` / `{kpiN_value}` | 封面/尾页KPI卡片 | 第N个指标的标签和数值 |
+| `{chapterN_title}` / `{chapterN_desc}` | 目录页 | 第N章标题和描述 |

+ 55 - 0
generate-data-report-ppt/references/visual-style-guide.md

@@ -0,0 +1,55 @@
+# 视觉样式规范
+
+## 幻灯片尺寸
+
+- **比例**:16:9 宽屏
+- **EMU 尺寸**:16256000 × 9144000 EMU
+- **英寸换算**:约 17.78" × 10"
+
+## 配色方案
+
+| 角色 | 色值 | 用途 |
+|------|------|------|
+| 主色 | `#2E5B8B` | 顶部蓝线、分隔线、标题强调、页眉标题 |
+| 辅色 | `#5B9BD5` | 图表主色、CONTENTS标签、数字高亮 |
+| 深色背景 | `#1F3A5C` | 封面左侧块、深色装饰 |
+| 白色 | `#FFFFFF` | 页眉背景、内容区背景、封面标题 |
+| 浅灰背景 | `#F2F2F2` | 目录页/尾页背景、底部来源条 |
+| 卡片浅蓝 | `#E7F0F7` | KPI卡片背景 |
+| 深灰文字 | `#333333` | 正文、主标题 |
+| 中灰文字 | `#666666` | 副标题、次要信息 |
+| 浅灰线条 | `#D9D9D9` | 分隔线、网格线 |
+
+## 字体规范
+
+| 层级 | 字体 | 大小 | 字重 | 颜色 |
+|------|------|------|------|------|
+| 封面大标题 | 微软雅黑 | 44pt | Bold | 白色 |
+| 封面副标题 | 微软雅黑 | 32pt | Regular | 白色 |
+| 页面主标题 | 微软雅黑 | 24pt | Bold | `#333333` |
+| 页眉报告名 | 微软雅黑 | 20pt | Bold | `#2E5B8B` |
+| 页眉日期 | 微软雅黑 | 16pt | Regular | `#333333` |
+| 正文/洞察 | 微软雅黑 | 14-18pt | Regular | `#333333` |
+| KPI数值 | Arial | 28-36pt | Bold | `#2E5B8B` |
+| KPI标签 | 微软雅黑 | 12-14pt | Regular | `#666666` |
+| 底部来源 | 微软雅黑 | 10pt | Regular | `#888888` |
+| 英文标签 | Arial | 14pt | Regular | `#5B9BD5` |
+
+## 布局间距
+
+| 元素 | 位置(EMU) | 尺寸(EMU) |
+|------|------------|------------|
+| 顶部蓝线 | (0, 0) | (全宽, 50800) |
+| 页眉白底 | (0, 50800) | (全宽, 558800) |
+| 分隔蓝线 | (0, 609600) | (全宽, 50800) |
+| 页面标题区 | (762000, 914400) | (11430000, 508000) |
+| 内容主区域 | (762000, 1524000) | (14732000, 7112000) |
+| 底部灰条 | (0, 8824000) | (全宽, 320000) |
+| 底部来源文字 | (762000, 8824000) | (7620000, 320000) |
+| 底部页码 | (13970000, 8824000) | (1778000, 320000) |
+
+## 形状样式
+
+- **KPI卡片**:圆角矩形(ROUNDED_RECTANGLE),填充 `#E7F0F7`,无边框
+- **告警卡片**:矩形,左侧带 50800 EMU 宽度的色条(严重=红色/警告=橙色/关注=蓝色)
+- **分隔线**:高度 0-50800 EMU 的矩形,填充 `#D9D9D9` 或 `#2E5B8B`

+ 567 - 0
generate-data-report-ppt/scripts/chart_factory.py

@@ -0,0 +1,567 @@
+"""
+Native editable chart factory using python-pptx.
+NO matplotlib / PNG insertion. All charts are native PowerPoint objects.
+"""
+from pptx.chart.data import ChartData, CategoryChartData
+from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION, XL_TICK_MARK, XL_DATA_LABEL_POSITION
+from pptx.enum.text import PP_ALIGN
+from pptx.util import Emu, Pt
+from pptx.dml.color import RGBColor
+
+# Color palette — aligned with reference design theme YAML
+C_BLUE = RGBColor(0x1E, 0x3A, 0x5F)       # primary
+C_BLUE_DARK = RGBColor(0x1E, 0x3A, 0x5F)  # primary dark
+C_ACCENT = RGBColor(0x10, 0xB9, 0x81)     # accent (growth)
+C_ACCENT_NEG = RGBColor(0xEF, 0x44, 0x44) # accentNeg (decline)
+C_ORANGE = RGBColor(0xED, 0x7D, 0x31)
+C_GRAY = RGBColor(0x64, 0x74, 0x8B)       # secondary
+C_GREEN = RGBColor(0x10, 0xB9, 0x81)
+C_RED = RGBColor(0xEF, 0x44, 0x44)
+C_TEXT = RGBColor(0x33, 0x33, 0x33)
+C_GRID = RGBColor(0xD9, 0xD9, 0xD9)
+C_WHITE = RGBColor(0xFF, 0xFF, 0xFF)
+
+# Default series colors for multi-color charts like doughnut/pie
+DEFAULT_COLORS = [
+    RGBColor(0x1E, 0x3A, 0x5F),  # primary
+    RGBColor(0x10, 0xB9, 0x81),  # accent
+    RGBColor(0xED, 0x7D, 0x31),  # orange
+    RGBColor(0x64, 0x74, 0x8B),  # secondary
+    RGBColor(0xEF, 0x44, 0x44),  # red
+    RGBColor(0x70, 0x70, 0x70),  # dark gray
+    RGBColor(0x44, 0x72, 0xC4),  # indigo
+    RGBColor(0x10, 0xB9, 0x81),  # accent2
+]
+
+
+def _apply_common_style(chart, show_legend=False, category_axis_title=None, value_axis_title=None):
+    """Apply common styling to a chart. Safe for all chart types."""
+    chart.has_title = False
+    
+    # Legend handling
+    if show_legend:
+        chart.has_legend = True
+        chart.legend.position = XL_LEGEND_POSITION.BOTTOM
+        chart.legend.include_in_layout = False
+        chart.legend.font.size = Pt(10)
+        chart.legend.font.name = '微软雅黑'
+    else:
+        chart.has_legend = False
+
+    # Category axis (not all charts have one, e.g. DOUGHNUT)
+    try:
+        cat_axis = chart.category_axis
+        if cat_axis:
+            cat_axis.tick_labels.font.size = Pt(10)
+            cat_axis.tick_labels.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
+            cat_axis.tick_labels.font.name = '微软雅黑'
+            cat_axis.format.line.fill.background()
+            try:
+                cat_axis.major_tick_mark = XL_TICK_MARK.NONE
+            except Exception:
+                pass
+            if category_axis_title:
+                try:
+                    cat_axis.has_title = True
+                    cat_axis.axis_title.text_frame.text = category_axis_title
+                    cat_axis.axis_title.text_frame.paragraphs[0].font.size = Pt(10)
+                    cat_axis.axis_title.text_frame.paragraphs[0].font.name = '微软雅黑'
+                    cat_axis.axis_title.text_frame.paragraphs[0].font.color.rgb = RGBColor(0x66, 0x66, 0x66)
+                except Exception:
+                    pass
+    except Exception:
+        pass
+
+    # Value axis
+    try:
+        val_axis = chart.value_axis
+        if val_axis:
+            val_axis.tick_labels.font.size = Pt(10)
+            val_axis.tick_labels.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
+            val_axis.tick_labels.font.name = '微软雅黑'
+            val_axis.format.line.fill.background()
+            try:
+                val_axis.major_tick_mark = XL_TICK_MARK.NONE
+            except Exception:
+                pass
+            try:
+                if val_axis.has_major_gridlines:
+                    val_axis.major_gridlines.format.line.color.rgb = C_GRID
+                    val_axis.major_gridlines.format.line.width = Pt(0.75)
+            except Exception:
+                pass
+            if value_axis_title:
+                try:
+                    val_axis.has_title = True
+                    val_axis.axis_title.text_frame.text = value_axis_title
+                    val_axis.axis_title.text_frame.paragraphs[0].font.size = Pt(10)
+                    val_axis.axis_title.text_frame.paragraphs[0].font.name = '微软雅黑'
+                    val_axis.axis_title.text_frame.paragraphs[0].font.color.rgb = RGBColor(0x66, 0x66, 0x66)
+                except Exception:
+                    pass
+    except Exception:
+        pass
+
+    # Plot area & chart area background cleanup
+    try:
+        chart.plot_area.format.fill.background()
+        chart.plot_area.format.line.fill.background()
+    except Exception:
+        pass
+    try:
+        chart.format.fill.background()
+        chart.format.line.fill.background()
+    except Exception:
+        pass
+
+
+def _apply_data_labels(series, show_value=True, show_percent=False, font_size=Pt(10),
+                       position=XL_DATA_LABEL_POSITION.OUTSIDE_END, number_format=None):
+    """Add data labels to a series with safe formatting."""
+    series.has_data_labels = True
+    for point in series.points:
+        dl = point.data_label
+        dl.font.size = font_size
+        dl.font.color.rgb = C_TEXT
+        dl.font.name = '微软雅黑'
+        if show_percent and hasattr(dl, 'show_percent'):
+            try:
+                dl.show_percent = True
+            except Exception:
+                pass
+        if show_value and hasattr(dl, 'show_value'):
+            try:
+                dl.show_value = True
+            except Exception:
+                pass
+        try:
+            dl.position = position
+        except Exception:
+            pass
+        if number_format and hasattr(dl, 'number_format'):
+            try:
+                dl.number_format = number_format
+            except Exception:
+                pass
+
+
+def _set_series_color(series, color):
+    """Safely set series fill color."""
+    try:
+        series.format.fill.solid()
+        series.format.fill.fore_color.rgb = color
+    except Exception:
+        pass
+
+
+def add_column_chart(slide, categories, values, left, top, width, height,
+                     series_name='数值', color=C_BLUE, show_data_labels=True,
+                     second_series=None, second_color=C_ORANGE,
+                     category_axis_title=None, value_axis_title=None):
+    """Add a clustered column chart."""
+    chart_data = ChartData()
+    chart_data.categories = categories
+    chart_data.add_series(series_name, values)
+    if second_series:
+        chart_data.add_series(second_series[0], second_series[1])
+
+    chart = slide.shapes.add_chart(
+        XL_CHART_TYPE.COLUMN_CLUSTERED,
+        left, top, width, height,
+        chart_data
+    ).chart
+
+    _apply_common_style(chart, show_legend=bool(second_series),
+                        category_axis_title=category_axis_title,
+                        value_axis_title=value_axis_title)
+
+    # Color first series
+    _set_series_color(chart.series[0], color)
+    if second_series and len(chart.series) > 1:
+        _set_series_color(chart.series[1], second_color)
+
+    if show_data_labels:
+        _apply_data_labels(chart.series[0])
+        if second_series and len(chart.series) > 1:
+            _apply_data_labels(chart.series[1])
+
+    return chart
+
+
+def add_bar_chart(slide, categories, values, left, top, width, height,
+                  series_name='数值', color=C_BLUE, reverse_order=True,
+                  show_data_labels=True,
+                  category_axis_title=None, value_axis_title=None):
+    """Add a clustered bar chart (horizontal)."""
+    chart_data = ChartData()
+    chart_data.categories = categories
+    chart_data.add_series(series_name, values)
+
+    chart = slide.shapes.add_chart(
+        XL_CHART_TYPE.BAR_CLUSTERED,
+        left, top, width, height,
+        chart_data
+    ).chart
+
+    _apply_common_style(chart, show_legend=False,
+                        category_axis_title=category_axis_title,
+                        value_axis_title=value_axis_title)
+
+    if reverse_order:
+        try:
+            chart.category_axis.reverse_order = True
+        except Exception:
+            pass
+
+    _set_series_color(chart.series[0], color)
+    if show_data_labels:
+        _apply_data_labels(chart.series[0], position=XL_DATA_LABEL_POSITION.OUTSIDE_END)
+
+    return chart
+
+
+def add_horizontal_bar_chart(slide, categories, values, left, top, width, height,
+                             series_name='数值', color=C_BLUE, reverse_order=True,
+                             show_data_labels=True, data_label_format=None,
+                             category_axis_title=None, value_axis_title=None):
+    """Add a horizontal bar chart (alias for add_bar_chart with enhanced defaults)."""
+    return add_bar_chart(
+        slide, categories, values, left, top, width, height,
+        series_name=series_name, color=color, reverse_order=reverse_order,
+        show_data_labels=show_data_labels,
+        category_axis_title=category_axis_title,
+        value_axis_title=value_axis_title
+    )
+
+
+def add_line_chart(slide, categories, values, left, top, width, height,
+                   series_name='数值', color=C_BLUE, marker_size=7,
+                   show_data_labels=False, second_series=None, second_color=C_ORANGE,
+                   category_axis_title=None, value_axis_title=None):
+    """Add a line chart with markers."""
+    chart_data = ChartData()
+    chart_data.categories = categories
+    chart_data.add_series(series_name, values)
+    if second_series:
+        chart_data.add_series(second_series[0], second_series[1])
+
+    chart = slide.shapes.add_chart(
+        XL_CHART_TYPE.LINE_MARKERS,
+        left, top, width, height,
+        chart_data
+    ).chart
+
+    _apply_common_style(chart, show_legend=bool(second_series),
+                        category_axis_title=category_axis_title,
+                        value_axis_title=value_axis_title)
+
+    chart.series[0].format.line.color.rgb = color
+    chart.series[0].format.line.width = Pt(2.5)
+    try:
+        chart.series[0].marker.style = 1  # circle
+        chart.series[0].marker.size = marker_size
+    except Exception:
+        pass
+
+    if second_series and len(chart.series) > 1:
+        chart.series[1].format.line.color.rgb = second_color
+        chart.series[1].format.line.width = Pt(2.5)
+        try:
+            chart.series[1].marker.style = 1
+            chart.series[1].marker.size = marker_size
+        except Exception:
+            pass
+
+    if show_data_labels:
+        _apply_data_labels(chart.series[0])
+
+    return chart
+
+
+def add_pie_chart(slide, categories, values, left, top, width, height,
+                  colors=None, show_data_labels=True, show_legend=True, show_percent=True):
+    """Add a pie chart with legend and data labels."""
+    chart_data = ChartData()
+    chart_data.categories = categories
+    chart_data.add_series('占比', values)
+
+    chart = slide.shapes.add_chart(
+        XL_CHART_TYPE.PIE,
+        left, top, width, height,
+        chart_data
+    ).chart
+
+    _apply_common_style(chart, show_legend=show_legend)
+
+    # Color each point individually
+    series = chart.series[0]
+    point_colors = colors if colors else DEFAULT_COLORS
+    for i, point in enumerate(series.points):
+        try:
+            point.format.fill.solid()
+            point.format.fill.fore_color.rgb = point_colors[i % len(point_colors)]
+        except Exception:
+            pass
+
+    total = sum(values) if values else 0
+    if show_data_labels:
+        series.has_data_labels = True
+        for i, point in enumerate(series.points):
+            dl = point.data_label
+            dl.font.size = Pt(9)
+            dl.font.color.rgb = C_TEXT
+            dl.font.name = '微软雅黑'
+            val = values[i] if i < len(values) else 0
+            pct = val / total * 100 if total else 0
+            # Compact label: fit inside slice; show value+percent, omit long category name
+            if pct >= 15:
+                dl.text_frame.text = f'{val}单\n({pct:.1f}%)'
+            else:
+                dl.text_frame.text = f'{pct:.1f}%'
+            dl.show_value = False
+            dl.show_percent = False
+            dl.show_category_name = False
+            try:
+                dl.position = XL_DATA_LABEL_POSITION.CENTER
+            except Exception:
+                pass
+
+    return chart
+
+
+def add_doughnut_chart(slide, categories, values, left, top, width, height,
+                       colors=None, hole_size=0.5, show_data_labels=True,
+                       show_legend=True, show_percent=True, ring_ratio=None):
+    """Add a doughnut chart with legend and data labels.
+    
+    Args:
+        ring_ratio: Alias for hole_size as a ratio (0.0-1.0). If provided, overrides hole_size.
+    """
+    if ring_ratio is not None:
+        hole_size = ring_ratio
+    
+    chart_data = ChartData()
+    chart_data.categories = categories
+    chart_data.add_series('占比', values)
+
+    chart = slide.shapes.add_chart(
+        XL_CHART_TYPE.DOUGHNUT,
+        left, top, width, height,
+        chart_data
+    ).chart
+
+    _apply_common_style(chart, show_legend=show_legend)
+
+    # Hole size
+    try:
+        if hasattr(chart.plots[0], 'hole_size'):
+            chart.plots[0].hole_size = int(hole_size * 100)
+    except Exception:
+        pass
+
+    # Color each point individually
+    series = chart.series[0]
+    point_colors = colors if colors else DEFAULT_COLORS
+    for i, point in enumerate(series.points):
+        try:
+            point.format.fill.solid()
+            point.format.fill.fore_color.rgb = point_colors[i % len(point_colors)]
+        except Exception:
+            pass
+
+    total = sum(values) if values else 0
+    if show_data_labels:
+        series.has_data_labels = True
+        for i, point in enumerate(series.points):
+            dl = point.data_label
+            dl.font.size = Pt(9)
+            dl.font.color.rgb = C_TEXT
+            dl.font.name = '微软雅黑'
+            val = values[i] if i < len(values) else 0
+            pct = val / total * 100 if total else 0
+            # Compact label: fit inside slice; show value+percent, omit long category name
+            if pct >= 15:
+                dl.text_frame.text = f'{val}单\n({pct:.1f}%)'
+            else:
+                dl.text_frame.text = f'{pct:.1f}%'
+            dl.show_value = False
+            dl.show_percent = False
+            dl.show_category_name = False
+            try:
+                dl.position = XL_DATA_LABEL_POSITION.CENTER
+            except Exception:
+                pass
+
+    return chart
+
+
+def add_funnel_chart(slide, categories, values, left, top, width, height,
+                     colors=None, show_data_labels=True, show_percent=True):
+    """Add a funnel-style chart using horizontal bar chart with reversed order.
+    
+    Displays stages from top to bottom with data labels showing quantity + percentage.
+    """
+    total = sum(values) if values else 0
+    chart_data = ChartData()
+    chart_data.categories = categories
+    chart_data.add_series('数量', values)
+
+    chart = slide.shapes.add_chart(
+        XL_CHART_TYPE.BAR_CLUSTERED,
+        left, top, width, height,
+        chart_data
+    ).chart
+
+    _apply_common_style(chart, show_legend=False)
+
+    # Reverse order so first category is at top
+    try:
+        chart.category_axis.reverse_order = True
+    except Exception:
+        pass
+
+    # Color each point individually
+    series = chart.series[0]
+    point_colors = colors if colors else DEFAULT_COLORS
+    for i, point in enumerate(series.points):
+        try:
+            point.format.fill.solid()
+            point.format.fill.fore_color.rgb = point_colors[i % len(point_colors)]
+        except Exception:
+            pass
+
+    if show_data_labels:
+        series.has_data_labels = True
+        for i, point in enumerate(series.points):
+            dl = point.data_label
+            dl.font.size = Pt(11)
+            dl.font.color.rgb = C_TEXT
+            dl.font.name = '微软雅黑'
+            dl.show_value = True
+            if show_percent and total > 0:
+                pct = values[i] / total * 100
+                # Custom label with value and percent
+                dl.text_frame.text = f"{values[i]} ({pct:.1f}%)"
+                dl.show_value = False
+                dl.show_percent = False
+            dl.position = XL_DATA_LABEL_POSITION.OUTSIDE_END
+
+    return chart
+
+
+def add_grouped_bar_chart(slide, categories, series_list, left, top, width, height,
+                          colors=None, show_data_labels=True, show_legend=True,
+                          category_axis_title=None, value_axis_title=None):
+    """Add a grouped (clustered) column/bar chart with multiple series.
+    
+    Args:
+        series_list: list of tuples [(series_name, values), ...]
+        colors: list of RGBColor for each series
+    """
+    chart_data = ChartData()
+    chart_data.categories = categories
+    for name, vals in series_list:
+        chart_data.add_series(name, vals)
+
+    chart = slide.shapes.add_chart(
+        XL_CHART_TYPE.COLUMN_CLUSTERED,
+        left, top, width, height,
+        chart_data
+    ).chart
+
+    _apply_common_style(chart, show_legend=show_legend,
+                        category_axis_title=category_axis_title,
+                        value_axis_title=value_axis_title)
+
+    point_colors = colors if colors else DEFAULT_COLORS
+    for i, series in enumerate(chart.series):
+        _set_series_color(series, point_colors[i % len(point_colors)])
+        if show_data_labels:
+            _apply_data_labels(series)
+
+    return chart
+
+
+def add_table(slide, rows, cols, data, left, top, width, height,
+              header_color=RGBColor(0x2E, 0x5B, 0x8B),
+              header_text_color=RGBColor(0xFF, 0xFF, 0xFF),
+              cell_color=RGBColor(0xFF, 0xFF, 0xFF),
+              alternate_color=RGBColor(0xF8, 0xFA, 0xFC),
+              font_size=Pt(11),
+              max_cell_chars=45):
+    """
+    Add a styled table with auto row height and text truncation.
+    data: list of lists, first row is header.
+    max_cell_chars: max characters per cell before truncating with '...'
+    """
+    from pptx.util import Pt
+    table = slide.shapes.add_table(rows, cols, left, top, width, height).table
+
+    # Set column widths proportionally
+    for i in range(cols):
+        table.columns[i].width = width // cols
+
+    # Calculate row height based on content
+    base_row_height = height // rows
+    for r_idx, row_data in enumerate(data):
+        for c_idx, val in enumerate(row_data):
+            if c_idx >= cols:
+                break
+            cell = table.cell(r_idx, c_idx)
+            text = str(val) if val is not None else ''
+            # Truncate if too long
+            if len(text) > max_cell_chars:
+                text = text[:max_cell_chars - 1] + '...'
+            cell.text = text
+
+            # Enable word wrap
+            cell.text_frame.word_wrap = True
+
+            # Font styling
+            paragraph = cell.text_frame.paragraphs[0]
+            paragraph.font.size = font_size
+            paragraph.font.name = '微软雅黑'
+            paragraph.font.color.rgb = header_text_color if r_idx == 0 else C_TEXT
+
+            # Background
+            cell.fill.solid()
+            if r_idx == 0:
+                cell.fill.fore_color.rgb = header_color
+                paragraph.font.bold = True
+            else:
+                if r_idx % 2 == 0 and alternate_color:
+                    cell.fill.fore_color.rgb = alternate_color
+                else:
+                    cell.fill.fore_color.rgb = cell_color
+
+            # Vertical alignment
+            cell.vertical_anchor = 1  # middle
+
+    return table
+
+
+if __name__ == '__main__':
+    from pptx import Presentation
+    prs = Presentation()
+    blank = prs.slide_layouts[6]
+    s = prs.slides.add_slide(blank)
+
+    add_column_chart(s, ['A', 'B', 'C'], [10, 20, 15],
+                     Emu(1000000), Emu(1000000), Emu(5000000), Emu(3000000))
+    add_bar_chart(s, ['X', 'Y', 'Z'], [30, 40, 25],
+                  Emu(1000000), Emu(4500000), Emu(5000000), Emu(3000000))
+    add_doughnut_chart(s, ['A', 'B', 'C'], [30, 40, 25],
+                       Emu(1000000), Emu(8000000), Emu(3000000), Emu(2000000),
+                       show_legend=True)
+    add_pie_chart(s, ['A', 'B', 'C'], [30, 40, 25],
+                  Emu(4500000), Emu(8000000), Emu(3000000), Emu(2000000),
+                  show_legend=True)
+    add_funnel_chart(s, ['Stage1', 'Stage2', 'Stage3'], [100, 60, 30],
+                     Emu(1000000), Emu(11000000), Emu(5000000), Emu(3000000))
+    add_grouped_bar_chart(s, ['A', 'B', 'C'], [
+        ('本期', [10, 20, 15]),
+        ('上期', [8, 18, 12])
+    ], Emu(6000000), Emu(11000000), Emu(5000000), Emu(3000000))
+
+    prs.save('chart_test.pptx')
+    print('Saved chart_test.pptx')

+ 249 - 0
generate-data-report-ppt/scripts/data_loader.py

@@ -0,0 +1,249 @@
+"""
+Excel data loader for daily/weekly/monthly report generation.
+"""
+import pandas as pd
+from datetime import datetime, timedelta
+import re
+import warnings
+
+# Field mapping: Excel column name -> internal field name
+FIELD_MAP = {
+    '序号': 'seq',
+    '目的国家': 'country',
+    '合同号': 'contract_no',
+    '用户名称/公司': 'customer',
+    '意向车型及数量': 'product_info',
+    '订单总数量': 'order_qty',
+    '负责人': 'owner',
+    '当前状态': 'status',
+    '拟定合同时间': 'contract_date',
+    '跟单天数': 'tracking_days',
+    '定金支付时间': 'deposit_date',
+    '订金认领时间': 'deposit_claim_date',
+    '订单生成时间': 'order_gen_date',
+    '价格评审时间': 'price_review_date',
+    '合同评审时间': 'contract_review_date',
+    '合同提交盖章申请时间': 'seal_apply_date',
+    '合同盖章时间': 'seal_date',
+    '车辆下线入库状态': 'inventory_status',
+    '尾款支付时间': 'final_pay_date',
+    '尾款认领时间': 'final_claim_date',
+    '智慧关务信息维护': 'customs_date',
+    '许可证办理时间': 'license_date',
+    '车辆发运时间': 'ship_date',
+    '预计开票时间': 'invoice_date',
+    '今日进度更新': 'progress_update',
+    '是否更新': 'is_updated',
+    '支持需求': 'support_request',
+    '4月交付': 'deliver_apr',
+    '5月预测': 'forecast_may',
+}
+
+STATUS_ORDER = ['A', 'B', 'C', 'D', 'E', 'F']
+STATUS_LABELS = {
+    'A': '合同拟定中',
+    'B': '已锁定合同待付订金',
+    'C': '已付订金待生产',
+    'D': '已生产待付尾款',
+    'E': '已付尾款待发运',
+    'F': '已发运',
+}
+
+
+def _normalize_status(val):
+    """Extract status code A-F from status string."""
+    if pd.isna(val):
+        return None
+    s = str(val).strip()
+    # Match pattern like "A(合同拟定中)" or "A"
+    m = re.match(r'^([A-F])', s)
+    if m:
+        return m.group(1)
+    return None
+
+
+def _parse_date(val):
+    """Parse various date formats."""
+    if pd.isna(val):
+        return None
+    if isinstance(val, datetime):
+        return val
+    for fmt in ('%Y-%m-%d', '%Y/%m/%d', '%Y年%m月%d日'):
+        try:
+            return datetime.strptime(str(val).strip(), fmt)
+        except ValueError:
+            continue
+    return None
+
+
+def _sheet_name_for_date(date: datetime) -> str:
+    """Convert datetime to expected sheet name."""
+    return date.strftime('%Y年%m月%d日')
+
+
+def load_workbook_metadata(filepath: str) -> dict:
+    """Return workbook metadata: sheet names, date range."""
+    xl = pd.ExcelFile(filepath)
+    sheets = xl.sheet_names
+    dates = []
+    for s in sheets:
+        try:
+            d = datetime.strptime(s, '%Y年%m月%d日')
+            dates.append(d)
+        except ValueError:
+            continue
+    dates.sort()
+    return {
+        'sheets': sheets,
+        'date_range': (dates[0], dates[-1]) if dates else (None, None),
+        'total_days': len(dates),
+    }
+
+
+def load_daily(filepath: str, date: datetime) -> pd.DataFrame:
+    """Load single-day order data."""
+    sheet = _sheet_name_for_date(date)
+    df = pd.read_excel(filepath, sheet_name=sheet)
+    return _clean_dataframe(df)
+
+
+def load_date_range(filepath: str, start: datetime, end: datetime) -> pd.DataFrame:
+    """Load and concatenate data across a date range [start, end]."""
+    xl = pd.ExcelFile(filepath)
+    frames = []
+    current = start
+    while current <= end:
+        sheet = _sheet_name_for_date(current)
+        if sheet in xl.sheet_names:
+            df = pd.read_excel(filepath, sheet_name=sheet)
+            df['_data_date'] = current
+            frames.append(df)
+        current += timedelta(days=1)
+    if not frames:
+        raise ValueError(f"No data found between {start.date()} and {end.date()}")
+    combined = pd.concat(frames, ignore_index=True)
+    return _clean_dataframe(combined)
+
+
+def load_weekly(filepath: str, year: int, week_num: int, week_start_day=0) -> tuple:
+    """
+    Load data for a specific week.
+    Returns (current_week_df, prev_week_df).
+    week_start_day: 0=Monday, 6=Sunday
+    """
+    # Find the first day of the given week
+    # Simplified: assume data starts from a known reference
+    meta = load_workbook_metadata(filepath)
+    first_date, last_date = meta['date_range']
+    if first_date is None:
+        raise ValueError("No valid date sheets found")
+
+    # Find the Monday of the target week (using ISO week definition)
+    # Jan 4 is always in week 1
+    jan4 = datetime(year, 1, 4)
+    # Adjust to Monday
+    jan4_monday = jan4 - timedelta(days=jan4.weekday())
+    target_monday = jan4_monday + timedelta(weeks=week_num - 1)
+    target_sunday = target_monday + timedelta(days=6)
+
+    # Clamp to available data range
+    start = max(target_monday, first_date)
+    end = min(target_sunday, last_date)
+
+    current = load_date_range(filepath, start, end)
+
+    # Previous week
+    prev_start = start - timedelta(days=7)
+    prev_end = end - timedelta(days=7)
+    if prev_start >= first_date:
+        previous = load_date_range(filepath, prev_start, prev_end)
+    else:
+        previous = pd.DataFrame(columns=current.columns)
+
+    return current, previous
+
+
+def load_monthly(filepath: str, year: int, month: int) -> tuple:
+    """
+    Load data for a specific month.
+    Returns (current_month_df, prev_month_df, yoy_month_df).
+    """
+    start = datetime(year, month, 1)
+    # Last day of month
+    if month == 12:
+        end = datetime(year + 1, 1, 1) - timedelta(days=1)
+    else:
+        end = datetime(year, month + 1, 1) - timedelta(days=1)
+
+    current = load_date_range(filepath, start, end)
+
+    # Previous month
+    if month == 1:
+        prev_start = datetime(year - 1, 12, 1)
+        prev_end = datetime(year, 1, 1) - timedelta(days=1)
+    else:
+        prev_start = datetime(year, month - 1, 1)
+        prev_end = datetime(year, month, 1) - timedelta(days=1)
+
+    try:
+        previous = load_date_range(filepath, prev_start, prev_end)
+    except ValueError:
+        previous = pd.DataFrame(columns=current.columns)
+
+    # YoY (same month last year)
+    yoy_start = datetime(year - 1, month, 1)
+    if month == 12:
+        yoy_end = datetime(year, 1, 1) - timedelta(days=1)
+    else:
+        yoy_end = datetime(year - 1, month + 1, 1) - timedelta(days=1)
+
+    try:
+        yoy = load_date_range(filepath, yoy_start, yoy_end)
+    except ValueError:
+        yoy = pd.DataFrame(columns=current.columns)
+
+    return current, previous, yoy
+
+
+def _clean_dataframe(df: pd.DataFrame) -> pd.DataFrame:
+    """Rename columns, parse dates, clean statuses."""
+    # Rename known columns
+    rename_map = {k: v for k, v in FIELD_MAP.items() if k in df.columns}
+    df = df.rename(columns=rename_map)
+
+    # Normalize status
+    if 'status' in df.columns:
+        df['status_code'] = df['status'].apply(_normalize_status)
+
+    # Parse numeric fields
+    if 'order_qty' in df.columns:
+        df['order_qty'] = pd.to_numeric(df['order_qty'], errors='coerce')
+
+    # Parse date fields
+    date_fields = ['contract_date', 'deposit_date', 'order_gen_date',
+                   'price_review_date', 'contract_review_date', 'seal_apply_date',
+                   'seal_date', 'final_pay_date', 'customs_date', 'license_date',
+                   'ship_date', 'invoice_date']
+    for field in date_fields:
+        if field in df.columns:
+            df[field] = df[field].apply(_parse_date)
+
+    # Tracking days
+    if 'tracking_days' in df.columns:
+        df['tracking_days'] = pd.to_numeric(df['tracking_days'], errors='coerce')
+
+    # Boolean updated
+    if 'is_updated' in df.columns:
+        df['is_updated_flag'] = df['is_updated'].astype(str).str.strip() == '是'
+
+    return df
+
+
+if __name__ == '__main__':
+    import sys
+    if len(sys.argv) > 1:
+        fp = sys.argv[1]
+        meta = load_workbook_metadata(fp)
+        print(f"Sheets: {meta['sheets'][:5]}...")
+        print(f"Date range: {meta['date_range'][0]} ~ {meta['date_range'][1]}")
+        print(f"Total days: {meta['total_days']}")

+ 1679 - 0
generate-data-report-ppt/scripts/deep_insights.py

@@ -0,0 +1,1679 @@
+"""
+Deep-analysis insight generators for weekly and monthly reports.
+Each function returns list[dict] with {'title': str, 'content': str}.
+"""
+
+
+# =============================================================================
+# WEEKLY INSIGHTS
+# =============================================================================
+
+def weekly_insights(page_type: str, metrics: dict, context: dict) -> list[dict]:
+    """Dispatch to specific weekly page insight functions."""
+    dispatch = {
+        'weekly_summary': _insight_weekly_summary,
+        'weekly_trend': _insight_weekly_trend,
+        'weekly_wow': _insight_weekly_wow,
+        'weekly_region': _insight_weekly_region,
+        'weekly_country': _insight_weekly_country,
+        'weekly_team': _insight_weekly_team,
+        'weekly_issue': _insight_weekly_issue,
+        'weekly_plan': _insight_weekly_plan,
+    }
+    fn = dispatch.get(page_type)
+    return fn(metrics, context) if fn else []
+
+
+def _insight_weekly_summary(metrics: dict, context: dict) -> list[dict]:
+    """Page 2: 周内节奏分析、月度进度、关键驱动因素"""
+    items = []
+    daily_trend = metrics.get('daily_trend', {})
+    total_qty = metrics.get('total_qty', 0)
+    tracking_orders = metrics.get('tracking_orders', 0)
+    prev_tracking_orders = metrics.get('prev_tracking_orders', 0)
+    top_countries = metrics.get('top_countries', {})
+    region_dist = metrics.get('region_dist', {})
+
+    # ① 周内节奏分析
+    if daily_trend:
+        dates = list(daily_trend.keys())
+        vals = list(daily_trend.values())
+        peak_idx = max(range(len(vals)), key=lambda i: vals[i])
+        low_idx = min(range(len(vals)), key=lambda i: vals[i])
+        peak_date, peak_val = dates[peak_idx], vals[peak_idx]
+        low_date, low_val = dates[low_idx], vals[low_idx]
+        prev_avg = metrics.get('prev_avg_daily_orders', 0)
+        above_avg = sum(1 for v in vals if prev_avg and v > prev_avg)
+        rhythm = (
+            f'本周订单呈"{peak_date}冲高、{low_date}回落"特征,峰值{peak_val}单、低谷{low_val}单,'
+            f'波动幅度{_safe_div(peak_val, low_val) if low_val else 0:.1f}倍。'
+            f'{"高于" if above_avg >= len(vals)//2 else "低于"}上周均值天数占比{above_avg}/{len(vals)},'
+            f'整体节奏{"前移" if peak_idx <= len(vals)//2 else "后移"}。'
+            f'建议分析低谷日是否受节假日或客户账期影响,优化排产与沟通节奏。'
+        )
+    else:
+        rhythm = (
+            f'本周累计订单{tracking_orders}单、{total_qty}台,'
+            f'日均{metrics.get("avg_daily_orders", 0)}单。'
+            f'由于缺乏分日数据,暂无法识别周内节奏特征。'
+            f'建议完善日报 granularity,以便识别周几效应并优化资源投放节奏。'
+        )
+    items.append({'title': '📅 周内节奏分析', 'content': rhythm})
+
+    # ② 与上周对比偏移
+    wow_pct = _pct_change(tracking_orders, prev_tracking_orders)
+    if prev_tracking_orders:
+        shift = (
+            f'本周{tracking_orders}单较上周{prev_tracking_orders}单{_fmt_chg_dir(wow_pct)}{_fmt_pct(wow_pct)},'
+            f'{"呈加速态势" if (wow_pct or 0) > 10 else "增长放缓" if (wow_pct or 0) > 0 else "出现回落"}。'
+            f'若趋势持续,下月累计将{"超额" if (wow_pct or 0) > 0 else "缺口"}约{abs((wow_pct or 0)):.0f}%月度目标。'
+            f'建议结合pipeline深度评估:增长来自存量转化还是新增签约,两种来源的可持续性差异显著。'
+        )
+    else:
+        shift = (
+            f'本周订单量{tracking_orders}单,缺乏上周同期数据作为基准。'
+            f'建议尽快建立周环比追踪机制,识别趋势方向。当前可依据日均{metrics.get("avg_daily_orders", 0)}单推算月度节奏,'
+            f'若保持稳定,整月预计约{metrics.get("avg_daily_orders", 0) * 30:.0f}单。'
+        )
+    items.append({'title': '📈 周环比趋势偏移', 'content': shift})
+
+    # ③ 月度进度推演
+    monthly_target = context.get('monthly_target', 0)
+    if monthly_target and total_qty:
+        progress_pct = round(total_qty / monthly_target * 100, 1)
+        week_of_month = context.get('week_of_month', 1)
+        expected = week_of_month / 4 * 100
+        gap = progress_pct - expected
+        progress = (
+            f'本周完成{total_qty}台,占月度目标{monthly_target}台的{progress_pct}%,'
+            f'按"四周均衡"预期应达{expected:.1f}%,{"领先" if gap >= 0 else "落后"}{abs(gap):.1f}个百分点。'
+            f'{"若保持当前 pace,月度有望超额完成。" if gap >= 0 else "剩余周需加速追赶,建议启动冲刺机制。"}'
+            f'重点关注最后一周产能与舱位是否匹配冲刺需求。'
+        )
+    else:
+        progress = (
+            f'本周完成{total_qty}台,因未设定月度目标或缺乏台数数据,暂无法计算进度占比。'
+            f'建议业务部门明确月度销量目标,以便周报自动生成目标达成率并触发预警。'
+            f'当前可先以本周为基准,要求后续每周环比增长不低于5%作为软约束。'
+        )
+    items.append({'title': '🎯 月度进度推演', 'content': progress})
+
+    # ④ 关键驱动因素(国家维度)
+    if top_countries:
+        top1_country = list(top_countries.keys())[0]
+        first_val = list(top_countries.values())[0]
+        top1_qty = first_val if isinstance(first_val, int) else first_val.get('qty', 0)
+        top3_qty = 0
+        for v in list(top_countries.values())[:3]:
+            top3_qty += v if isinstance(v, int) else v.get('qty', 0)
+        concentration = round(top3_qty / total_qty * 100, 1) if total_qty else 0
+        driver = (
+            f'本周Top 3国家贡献{top3_qty}台(占比{concentration}%),其中{top1_country}以{top1_qty}台领跑。'
+            f'{"大客户驱动特征明显,单国依赖度较高" if concentration > 40 else "国家分布相对均衡,抗波动能力较强"}。'
+            f'若Top 1国家订单来自单一客户,建议评估该客户下月复购概率;'
+            f'若来自多客户分散订单,则说明该国市场渗透进入良性循环。'
+        )
+    else:
+        driver = (
+            f'本周累计{total_qty}台,暂无国家维度分解数据。'
+            f'建议完善订单数据中国家字段,以便识别关键驱动市场并制定差异化资源投入策略。'
+            f'当前阶段可通过负责人访谈定性判断:本周增长主要来自老客户返单还是新市场突破。'
+        )
+    items.append({'title': '💡 关键驱动因素', 'content': driver})
+
+    # ⑤ 区域引擎识别
+    if region_dist:
+        regions = sorted(region_dist.items(), key=lambda x: x[1].get('qty', 0), reverse=True)
+        r1_name, r1_data = regions[0]
+        r1_qty = r1_data.get('qty', 0)
+        r1_pct = r1_data.get('pct', 0)
+        top_c = [c["country"] for c in r1_data.get("top_countries", [])[:2]]
+        region_text = (
+            f'{r1_name}本周贡献{r1_qty}台({r1_pct}%),为当前第一大区域引擎。'
+            f'其Top国家为{"、".join(top_c)}。'
+            f'建议对该区域实施"深耕+复制"策略:在成熟国家推进增值服务提升客单价,'
+            f'同时将成功经验复制至同区域二线国家,最大化区域协同效应。'
+        )
+    else:
+        region_text = (
+            f'本周订单{tracking_orders}单,缺乏区域维度数据。'
+            f'建议建立国家→区域映射表,以便自动识别区域引擎并评估资源投入ROI。'
+            f'当前可通过订单号前缀或客户归属地手动标注区域,作为过渡方案。'
+        )
+    items.append({'title': '🌍 区域引擎识别', 'content': region_text})
+
+    # ⑥ 结构健康度速览
+    avg_qty = metrics.get('avg_qty_per_order', 0)
+    prev_avg_qty = metrics.get('prev_avg_qty', 0) or context.get('prev_avg_qty', 0)
+    avg_chg = _pct_change(avg_qty, prev_avg_qty)
+    health = (
+        f'本周单均台数{avg_qty}台,{"较上周" + _fmt_chg_dir(avg_chg) + _fmt_pct(avg_chg) if prev_avg_qty else "基准待建立"}。'
+        f'{"大单占比提升,客户结构优化" if (avg_chg or 0) > 0 else "中小单为主,需关注大客户转化" if avg_qty else "数据缺失"}。'
+        f'结合{metrics.get("countries", 0)}个目的国分析,'
+        f'{"市场覆盖广但单国深度不足" if metrics.get("countries", 0) > 15 and avg_qty < 20 else ""}'
+        f'建议下周重点跟进单均台数大于50台的大单,锁定其对月度目标的贡献。'
+    )
+    items.append({'title': '⚖️ 结构健康度速览', 'content': health})
+
+    return items
+
+
+
+def _insight_weekly_trend(metrics: dict, context: dict) -> list[dict]:
+    """Page 3: 周内波动归因、与上周结构性差异、趋势持续性"""
+    items = []
+    daily_trend = metrics.get('daily_trend', {})
+    prev_daily_trend = context.get('prev_daily_trend', {})
+    tracking_orders = metrics.get('tracking_orders', 0)
+    prev_tracking_orders = metrics.get('prev_tracking_orders', 0)
+    forecast_next = metrics.get('forecast_next', 0)
+    total_qty = metrics.get('total_qty', 0)
+
+    # ① 周内波动归因
+    if daily_trend:
+        dates = list(daily_trend.keys())
+        vals = list(daily_trend.values())
+        avg_val = sum(vals) / len(vals)
+        low_days = [d for d, v in daily_trend.items() if v < avg_val * 0.7]
+        high_days = [d for d, v in daily_trend.items() if v > avg_val * 1.3]
+        fluctuation = (
+            f'本周日均{avg_val:.0f}单,{"、".join(high_days) if high_days else "无"}为显著高于均值的高活跃日,'
+            f'{"、".join(low_days) if low_days else "无"}为低于均值0.7倍的低谷日。'
+            f'低谷日需排查是否为节假日(当地法定假期)、客户月度/季度账期结算日,或系统批量录入延迟。'
+            f'建议建立"低谷日归因登记簿",记录每次低谷的外部因素,积累预测模型训练数据。'
+        )
+    else:
+        fluctuation = (
+            f'本周累计{tracking_orders}单,缺乏分日趋势数据。'
+            f'建议每日统计新增订单数,以便识别周内波动模式。当前可通过负责人日报定性判断:'
+            f'本周是否有1-2天明显低于其他工作日,若有,需排查外部干扰因素并提前制定应对措施。'
+        )
+    items.append({'title': '🔍 周内波动归因', 'content': fluctuation})
+
+    # ② 与上周结构性差异
+    if daily_trend and prev_daily_trend:
+        prev_vals = list(prev_daily_trend.values())
+        curr_vals = list(daily_trend.values())
+        curr_shape = "前高后低" if sum(curr_vals[:len(curr_vals)//2]) > sum(curr_vals[len(curr_vals)//2:]) else "前低后高"
+        prev_shape = "前高后低" if sum(prev_vals[:len(prev_vals)//2]) > sum(prev_vals[len(prev_vals)//2:]) else "前低后高"
+        structural = (
+            f'本周走势形态为"{curr_shape}",上周为"{prev_shape}",{"形态一致" if curr_shape == prev_shape else "形态发生反转"}。'
+            f'{"说明客户下单节奏稳定,周末效应或账期规律持续生效。" if curr_shape == prev_shape else "说明外部条件发生变化,可能受节假日错位或大单时点影响。"}'
+            f'建议对比两周的Top 3客户下单日期,判断结构差异来自系统性因素还是偶然大单。'
+        )
+    else:
+        structural = (
+            f'本周订单{tracking_orders}单,'
+            f'{"较上周" + _fmt_chg_dir(_pct_change(tracking_orders, prev_tracking_orders)) + _fmt_pct(_pct_change(tracking_orders, prev_tracking_orders)) if prev_tracking_orders else "缺乏上周分日数据"}。'
+            f'建议保留每周分日数据,以便进行走势形态对比(前高后低 vs 前低后高)。'
+            f'形态一致性是判断趋势可持续性的重要信号。'
+        )
+    items.append({'title': '📊 与上周结构性差异', 'content': structural})
+
+    # ③ 趋势持续性判断
+    wow_pct = _pct_change(tracking_orders, prev_tracking_orders)
+    if forecast_next:
+        sustain = (
+            f'Pipeline预测下月交付{forecast_next}台,结合本周{tracking_orders}单(wow {_fmt_pct(wow_pct)}),'
+            f'{"趋势向上且pipeline充足,持续性较强" if (wow_pct or 0) > 5 and forecast_next > total_qty * 0.8 else ""}'
+            f'{"趋势向好但pipeline不足,需警惕后继乏力" if (wow_pct or 0) > 5 and forecast_next <= total_qty * 0.5 else ""}'
+            f'{"趋势承压,pipeline也偏保守,双重承压" if (wow_pct or 0) < 0 else ""}'
+            f'。建议基于A+B阶段存量订单数评估真实转化潜力,'
+            f'若早期pipeline充足,则本周增长具备延续性;若依赖存量冲刺,则下周可能回落。'
+        )
+    else:
+        sustain = (
+            f'本周订单{tracking_orders}单(wow {_fmt_pct(wow_pct)}),因缺乏pipeline预测数据,'
+            f'趋势持续性需通过阶段结构间接判断。'
+            f'若A+B阶段订单占比大于40%,说明前期储备充足,下周有望维持;'
+            f'若E+F阶段占比过高,则本周增长可能来自集中交付的一次性脉冲,持续性存疑。'
+        )
+    items.append({'title': '🔮 趋势持续性判断', 'content': sustain})
+
+    # ④ 外部因素关联
+    support_cats = metrics.get('support_categories', {})
+    if support_cats:
+        top_cat = max(support_cats.items(), key=lambda x: x[1])
+        external = (
+            f'本周支持需求中"{top_cat[0]}"类最多({top_cat[1]}项),'
+            f'{"多为物流/船期问题,说明交付端承压,可能影响客户下单信心" if "物流" in top_cat[0] or "船期" in top_cat[0] else ""}'
+            f'{"多为财务/收款问题,说明资金端存在卡点,可能拖慢合同锁定节奏" if "财务" in top_cat[0] or "收款" in top_cat[0] else ""}'
+            f'。建议将该类问题的根因归类为流程/政策/外部三类,'
+            f'流程类内部优化,政策类提前预警,外部类建立替代方案(如备用船期/汇率对冲)。'
+        )
+    else:
+        external = (
+            f'本周暂无支持需求数据。建议主动收集:汇率波动、目的国进口政策调整、船期延误等外部事件,'
+            f'并评估其对本周订单节奏的干扰程度。'
+            f'建立"外部因素-订单波动"关联看板,可显著提升趋势归因的准确性。'
+        )
+    items.append({'title': '🌐 外部因素关联', 'content': external})
+
+    # ⑤ 异常点识别
+    if daily_trend:
+        vals = list(daily_trend.values())
+        mean_v = sum(vals) / len(vals)
+        std = (sum((v - mean_v) ** 2 for v in vals) / len(vals)) ** 0.5
+        outliers = [(d, v) for d, v in daily_trend.items() if abs(v - mean_v) > 1.5 * std]
+        if outliers:
+            outlier_text = (
+                f'本周{"、".join([d for d, _ in outliers])}出现异常波动,偏离均值{mean_v:.0f}单超过1.5倍标准差。'
+                f'异常日需区分"机会型"(大客户集中下单)与"风险型"(系统故障/数据补录)。'
+                f'建议对异常日逐单标注原因,长期积累后可建立异常检测规则,实现自动化预警。'
+            )
+        else:
+            outlier_text = (
+                f'本周各日订单围绕均值{mean_v:.0f}单正常波动,无显著异常点。'
+                f'说明业务运行平稳,外部干扰较少。建议保持当前运营节奏,'
+                f'同时将本周作为"平稳基线"保存,用于未来异常检测的对比基准。'
+            )
+    else:
+        outlier_text = (
+            f'缺乏分日数据,无法识别周内异常点。'
+            f'建议建立日报机制后,采用"均值+1.5倍标准差"规则自动标记异常交易日。'
+        )
+    items.append({'title': '⚠️ 异常点识别', 'content': outlier_text})
+
+    # ⑥ 下周走势预判
+    if daily_trend and prev_daily_trend:
+        curr_avg = sum(daily_trend.values()) / len(daily_trend)
+        prev_avg = sum(prev_daily_trend.values()) / len(prev_daily_trend)
+        momentum = _pct_change(curr_avg, prev_avg)
+        pred_low = int(curr_avg * 0.85)
+        pred_high = int(curr_avg * 1.15)
+        forecast = (
+            f'基于本周日均{curr_avg:.0f}单及环比动量{_fmt_pct(momentum)},'
+            f'预测下周日均订单区间{pred_low}-{pred_high}单,整周预计{pred_low * 5}-{pred_high * 5}单。'
+            f'{"若大客户持续下单,有望突破区间上限" if (momentum or 0) > 10 else ""}'
+            f'{"若外部因素未改善,可能回落至区间下限" if (momentum or 0) < 0 else ""}'
+            f'。建议每日晨会对照预测区间,偏差大于20%时触发专项复盘。'
+        )
+    else:
+        forecast = (
+            f'当前在跟订单{tracking_orders}单,基于历史经验,下周预计维持在'
+            f'{int(tracking_orders * 0.85)}-{int(tracking_orders * 1.15)}单区间。'
+            f'建议重点关注A→B阶段转化速率,这是下周增量的最可预测来源。'
+        )
+    items.append({'title': '📉 下周走势预判', 'content': forecast})
+
+    return items
+
+
+
+def _insight_weekly_wow(metrics: dict, context: dict) -> list[dict]:
+    """Page 4: 各环节转化效率、瓶颈环节深度诊断、库存/资金占用"""
+    items = []
+    status_wow = metrics.get('status_wow', {})
+    status_dist = metrics.get('status_dist', {})
+    total_orders = metrics.get('tracking_orders', 0)
+
+    def _get_status(name):
+        return status_dist.get(name, 0)
+
+    # ① 各环节转化效率
+    a_name, b_name = '合同拟定中', '已锁定合同待付订金'
+    a_curr = status_wow.get(a_name, {}).get('current', 0)
+    a_prev = status_wow.get(a_name, {}).get('previous', 0)
+    b_curr = status_wow.get(b_name, {}).get('current', 0)
+    b_prev = status_wow.get(b_name, {}).get('previous', 0)
+    if a_prev and b_prev:
+        prev_conv = round(b_prev / a_prev * 100, 1)
+        curr_conv = round(b_curr / a_curr * 100, 1) if a_curr else 0
+        conv_chg = _pct_change(curr_conv, prev_conv)
+        conv = (
+            f'A→B阶段转化率本周{curr_conv}%({b_curr}/{a_curr}),上周{prev_conv}%({b_prev}/{a_prev}),'
+            f'{_fmt_chg_dir(conv_chg)}{_fmt_pct(conv_chg)}。'
+            f'{"转化率提升,说明合同推进效率改善" if (conv_chg or 0) > 0 else "转化率下降,合同锁定遇阻"}。'
+            f'建议拆解转化失败的根因:客户侧(审批慢/资金未到位)vs 我方侧(条款争议/响应慢),针对性优化。'
+        )
+    else:
+        conv = (
+            f'本周A阶段{a_curr}单、B阶段{b_curr}单,因缺乏上周A/B分阶段数据,无法计算转化率变化。'
+            f'建议建立阶段快照机制,每周固定时点统计各阶段存量。当前可先设定A→B周转化目标为A阶段存量的15%,'
+            f'并落实到具体负责人。'
+        )
+    items.append({'title': '⚡ A→B转化效率', 'content': conv})
+
+    # ② 瓶颈环节深度诊断
+    changes = [(name, data.get('change_pct', 0) or 0) for name, data in status_wow.items()]
+    if changes:
+        worst = min(changes, key=lambda x: x[1])
+        best = max(changes, key=lambda x: x[1])
+        bottleneck = (
+            f'本周降幅最大环节为"{worst[0]}"({_fmt_pct(worst[1])}),增幅最大为"{best[0]}"(+{_fmt_pct(best[1])})。'
+            f'{"若降幅环节为E/F,说明交付端受阻;若为A/B,则前端获客或转化遇困。"}'
+            f'建议对{worst[0]}环节启动"五问法"根因分析:'
+            f'是流程卡点、资源不足、还是外部政策突变?找到根因后48h内出具改进方案。'
+        )
+    else:
+        bottleneck = (
+            f'本周各阶段环比数据不完整,暂无法识别瓶颈环节。'
+            f'建议建立每周阶段快照对比机制,重点监控C→D(生产)和D→E(尾款)两个关键转化节点。'
+            f'这两个节点直接决定交付周期和资金回笼速度,是 Weekly Review 的核心指标。'
+        )
+    items.append({'title': '📉 瓶颈环节深度诊断', 'content': bottleneck})
+
+    # ③ 库存与资金占用分析
+    c_curr = status_wow.get('已付订金待生产', {}).get('current', 0)
+    d_curr = status_wow.get('已生产待付尾款', {}).get('current', 0)
+    c_prev = status_wow.get('已付订金待生产', {}).get('previous', 0)
+    d_prev = status_wow.get('已生产待付尾款', {}).get('previous', 0)
+    cd_total = c_curr + d_curr
+    cd_prev = c_prev + d_prev
+    cd_chg = _pct_change(cd_total, cd_prev)
+    inventory = (
+        f'生产端(C+D)本周合计{cd_total}单,较上周{_fmt_chg_dir(cd_chg)}{_fmt_pct(cd_chg)}。'
+        f'{"生产端积压加重,资金与产能占用上升" if (cd_chg or 0) > 10 else "生产端平稳,库存压力可控" if abs(cd_chg or 0) <= 10 else "生产端去化加速,周转改善"}。'
+        f'D阶段{d_curr}单为核心风险点:已生产未收款,每单约占用产能和资金。'
+        f'建议对D阶段大于14天的订单启动专项催收,缩短资金占用周期。'
+    )
+    items.append({'title': '💰 库存与资金占用分析', 'content': inventory})
+
+    # ④ 发运端效率
+    e_curr = status_wow.get('已付尾款待发运', {}).get('current', 0)
+    f_curr = status_wow.get('已发运', {}).get('current', 0)
+    e_prev = status_wow.get('已付尾款待发运', {}).get('previous', 0)
+    ef_total = e_curr + f_curr
+    ef_text = (
+        f'发运端(E+F)本周合计{ef_total}单,其中待发运{e_curr}单、已发运{f_curr}单。'
+        f'待发运较上周{_fmt_chg_dir(_pct_change(e_curr, e_prev))}{_fmt_pct(_pct_change(e_curr, e_prev))}。'
+        f'{"待发运积压上升,需排查船期/舱位/报关瓶颈" if e_curr > (e_prev * 1.2 if e_prev else 0) else "发运节奏顺畅,交付闭环效率良好"}。'
+        f'建议物流部门每周提供船期表,销售据此与客户对齐收货预期,减少催单消耗的管理精力。'
+    )
+    items.append({'title': '🚢 发运端效率', 'content': ef_text})
+
+    # ⑤ 漏斗健康度综合评分
+    if total_orders:
+        early = _get_status('合同拟定中') + _get_status('已锁定合同待付订金')
+        mid = _get_status('已付订金待生产') + _get_status('已生产待付尾款')
+        late = _get_status('已付尾款待发运') + _get_status('已发运')
+        early_pct = round(early / total_orders * 100, 1)
+        mid_pct = round(mid / total_orders * 100, 1)
+        late_pct = round(late / total_orders * 100, 1)
+        health = (
+            f'本周漏斗结构:前期{early_pct}%({early}单)、中期{mid_pct}%({mid}单)、后期{late_pct}%({late}单)。'
+            f'{"前期占比偏高,pipeline充足但转化压力较大" if early_pct > 40 else ""}'
+            f'{"中期占比偏高,生产端承压,需关注产能瓶颈" if mid_pct > 40 else ""}'
+            f'{"后期占比偏高,交付冲刺期,需确保物流资源到位" if late_pct > 40 else ""}'
+            f'理想结构为前期35%-中期35%-后期30%,当前{"接近理想" if 30 <= early_pct <= 40 and 30 <= mid_pct <= 40 else "偏离理想,需针对性调整"}。'
+        )
+    else:
+        health = (
+            f'本周暂无订单数据,无法计算漏斗结构。'
+            f'建议建立标准化漏斗健康度评分模型:前期35%-中期35%-后期30%为基准,'
+            f'偏离超过10个百分点时自动触发预警并推荐改进动作。'
+        )
+    items.append({'title': '🏥 漏斗健康度综合评分', 'content': health})
+
+    return items
+
+
+
+def _insight_weekly_region(metrics: dict, context: dict) -> list[dict]:
+    """Page 5: 区域战略优先级、区域间协同、新兴市场孵化"""
+    items = []
+    region_dist = metrics.get('region_dist', {})
+    total_qty = metrics.get('total_qty', 0)
+    prev_region_dist = context.get('prev_region_dist', {})
+
+    if not region_dist:
+        return [
+            {'title': '💡 区域战略优先级', 'content': '本周暂无区域分布数据。建议完善订单数据中的国家与区域映射,以便识别明星区域与问题区域,优化资源投放策略。'},
+            {'title': '📈 区域间协同', 'content': '缺乏区域维度数据,暂无法评估区域间协同效应。建议收集相邻区域订单关联数据,识别是否存在客户转介绍或区域联动带来的增长。'},
+        ]
+
+    # ① 区域战略优先级矩阵
+    regions = []
+    for name, data in region_dist.items():
+        qty = data.get('qty', 0)
+        pct = data.get('pct', 0)
+        prev_qty = prev_region_dist.get(name, {}).get('qty', 0) if prev_region_dist else 0
+        growth = _pct_change(qty, prev_qty)
+        regions.append((name, qty, pct, growth))
+
+    stars = [r for r in regions if r[2] > 20 and (r[3] is None or r[3] > 0)]
+    cows = [r for r in regions if r[2] > 20 and (r[3] is not None and r[3] <= 0)]
+    questions = [r for r in regions if r[2] <= 20 and (r[3] is None or r[3] > 0)]
+    dogs = [r for r in regions if r[2] <= 20 and (r[3] is not None and r[3] <= 0)]
+
+    matrix = (
+        f'基于"占比×增速"矩阵分析:'
+        f'{"明星区域(高占比+增长):" + "、".join([r[0] for r in stars[:2]]) + ",应继续加大投入;" if stars else ""}'
+        f'{"现金牛(高占比+放缓):" + "、".join([r[0] for r in cows[:2]]) + ",维持投入收割利润;" if cows else ""}'
+        f'{"问题区域(低占比+增长):" + "、".join([r[0] for r in questions[:2]]) + ",需评估是否加大培育;" if questions else ""}'
+        f'{"瘦狗区域(低占比+下滑):" + "、".join([r[0] for r in dogs[:2]]) + ",考虑收缩或调整策略。" if dogs else ""}'
+    )
+    if not any([stars, cows, questions, dogs]):
+        matrix = (
+            f'本周各区域占比与增速数据不足以完成四象限分类。'
+            f'建议建立区域级周环比追踪,当区域数据完整后可自动生成波士顿矩阵并推荐资源配置策略。'
+        )
+    items.append({'title': '💡 区域战略优先级矩阵', 'content': matrix})
+
+    # ② 区域间协同效应
+    top_regions = sorted(regions, key=lambda x: x[1], reverse=True)[:3]
+    if len(top_regions) >= 2:
+        synergy = (
+            f'{top_regions[0][0]}本周贡献{top_regions[0][1]}台领跑,{top_regions[1][0]}以{top_regions[1][1]}台紧随其后。'
+            f'若两区域存在客户转介绍或同一代理商覆盖,则具备协同放大效应。'
+            f'建议复盘{top_regions[0][0]}的成功经验(车型偏好/渠道模式/定价策略),'
+            f'形成标准化打法后向{top_regions[1][0]}及同类区域复制,降低试错成本。'
+        )
+    else:
+        synergy = (
+            f'本周{top_regions[0][0]}为绝对主导区域,其他区域占比偏低。'
+            f'单极结构下协同效应有限,建议评估是否通过"成熟区带新区"机制,'
+            f'让成熟区域负责人兼任相邻新兴市场顾问,实现经验外溢。'
+        )
+    items.append({'title': '📈 区域间协同效应', 'content': synergy})
+
+    # ③ 新兴市场孵化
+    top8_countries = set()
+    for data in region_dist.values():
+        for c in data.get('top_countries', []):
+            top8_countries.add(c.get('country', ''))
+    all_countries_count = metrics.get('countries', 0)
+    if all_countries_count > len(top8_countries):
+        emerging = (
+            f'本周覆盖{all_countries_count}国,Top 8框架内国家{len(top8_countries)}个,'
+            f'另有{all_countries_count - len(top8_countries)}国为框架外新兴市场。'
+            f'若框架外国家本周出现首单或复购,说明市场孵化取得突破。'
+            f'建议对框架外国家建立"观察名单",单月订单大于3台即纳入正式跟踪,并匹配专项资源。'
+        )
+    else:
+        emerging = (
+            f'本周覆盖{all_countries_count}国,订单高度集中在Top国家。'
+            f'新兴市场孵化进度较慢,建议设定"每月突破1个新国家"的拓展目标,'
+            f'通过参加区域性车展、与当地经销商建立合作等方式扩大市场覆盖。'
+        )
+    items.append({'title': '🌱 新兴市场孵化', 'content': emerging})
+
+    # ④ 区域投入ROI评估
+    if regions:
+        growths = [r[3] for r in regions if r[3] is not None]
+        avg_growth = sum(growths) / len(growths) if growths else 0
+        roi = (
+            f'本周区域平均增速{_fmt_pct(avg_growth)}。'
+            f'{"增速为正的区域的投入产出比优于整体,建议将增量预算向这些区域倾斜" if avg_growth > 0 else "整体增速承压,建议收缩低效区域投入,集中资源保高潜市场"}。'
+            f'ROI评估应综合考虑订单量、利润率和售后成本:高订单低利润区域需谨慎追加投入,'
+            f'低订单高利润区域(如配件服务收入高)反而值得深耕。'
+        )
+    else:
+        roi = (
+            f'缺乏区域增速数据,暂无法计算投入ROI。'
+            f'建议建立区域级损益表,至少包含订单量、毛利率、物流成本、售后成本四项指标,'
+            f'每季度评估一次区域真实贡献度,优化资源分配。'
+        )
+    items.append({'title': '💰 区域投入ROI评估', 'content': roi})
+
+    # ⑤ 区域风险分散
+    top1_region_pct = max(r[2] for r in regions) if regions else 0
+    risk = (
+        f'第一大区域占比{top1_region_pct}%,{"集中度偏高,存在单区域政策/汇率波动风险" if top1_region_pct > 50 else "集中度适中,风险分散良好"}。'
+        f'{"建议3个月内将第一大区域占比压降至50%以下,通过培育第二梯队实现结构优化" if top1_region_pct > 50 else "建议继续巩固现有均衡格局,同时培育1-2个潜力区域作为第三极"}。'
+        f'区域多元化是抵御单一市场政策风险的最有效手段。'
+    )
+    items.append({'title': '⚠️ 区域风险分散', 'content': risk})
+
+    return items
+
+
+
+def _insight_weekly_country(metrics: dict, context: dict) -> list[dict]:
+    """Page 6: 国家组合健康度、大客户集中度、竞争格局"""
+    items = []
+    top_countries = metrics.get('top_countries', {})
+    top_countries_change = metrics.get('top_countries_change', {})
+    total_qty = metrics.get('total_qty', 0)
+    top6_concentration_pct = metrics.get('top6_concentration_pct', 0)
+
+    if not top_countries:
+        return [
+            {'title': '💡 国家组合健康度', 'content': '本周暂无国家分布数据。建议完善目的国家字段,以便评估国家组合集中度风险并制定分散策略。'},
+        ]
+
+    country_qty = {}
+    for c, v in top_countries.items():
+        country_qty[c] = v if isinstance(v, int) else v.get('qty', 0)
+
+    sorted_countries = sorted(country_qty.items(), key=lambda x: x[1], reverse=True)
+    top3_qty = sum(v for _, v in sorted_countries[:3])
+    top3_pct = round(top3_qty / total_qty * 100, 1) if total_qty else 0
+
+    # ① 国家组合健康度
+    health = (
+        f'Top 3国家合计{top3_qty}台({top3_pct}%),{"超过40%警戒线,单国波动对整体业绩影响显著" if top3_pct > 40 else "低于40%,国家分布相对健康"}。'
+        f'{"Top 6集中度" + str(top6_concentration_pct) + "%进一步印证了大客户/大国家驱动特征" if top6_concentration_pct > 60 else ""}'
+        f'建议设定"Top 3占比小于40%、Top 6小于65%"的组合健康目标,'
+        f'通过培育第二梯队国家(4-8名)逐步降低头部依赖。'
+    )
+    items.append({'title': '💡 国家组合健康度', 'content': health})
+
+    # ② 大客户集中度
+    top1_country, top1_qty = sorted_countries[0]
+    chg_data = top_countries_change.get(top1_country, {})
+    chg_pct = chg_data.get('change_pct') if isinstance(chg_data, dict) else None
+    concentration = (
+        f'{top1_country}本周{top1_qty}台领跑,环比{_fmt_pct(chg_pct) if chg_pct is not None else "—"}。'
+        f'若该国订单来自单一客户,则存在极大客户依赖风险,该客户流失将导致月度目标大幅缺口。'
+        f'建议对Top 1国家进行客户拆解:单一客户占比大于50%则触发红色预警,需立即启动新客户开发计划。'
+    )
+    items.append({'title': '👤 大客户集中度风险', 'content': concentration})
+
+    # ③ 国家增速梯队
+    if top_countries_change:
+        growing = [(c, d['change_pct']) for c, d in top_countries_change.items() if d.get('change_pct', 0) and d['change_pct'] > 0]
+        declining = [(c, d['change_pct']) for c, d in top_countries_change.items() if d.get('change_pct', 0) and d['change_pct'] < 0]
+        growing.sort(key=lambda x: x[1], reverse=True)
+        declining.sort(key=lambda x: x[1])
+        tier = (
+            f'国家增速梯队:上升最快{"为" + "、".join([c for c, _ in growing[:2]]) if growing else "暂无"},'
+            f'下滑最快{"为" + "、".join([c for c, _ in declining[:2]]) if declining else "暂无"}。'
+            f'上升国家需分析是政策红利还是客户拓展见效,判断可持续性;'
+            f'下滑国家需区分暂时性因素(节假日/账期)还是结构性因素(竞争加剧/需求萎缩),对症下药。'
+        )
+    else:
+        tier = (
+            f'本周缺乏国家维度环比数据,无法划分增速梯队。'
+            f'建议每周固定输出Top 10国家环比变化表,识别"黑马"与"掉队"国家,'
+            f'为资源动态调配提供数据依据。'
+        )
+    items.append({'title': '📊 国家增速梯队', 'content': tier})
+
+    # ④ 竞争格局判断
+    competition = (
+        f'若{top1_country}市场出现价格竞争或交付周期压缩,说明竞品正在渗透。'
+        f'建议建立"竞争信号"监测机制:客户询盘时提及竞品频次、询盘转化率变化、订单周期拉长等。'
+        f'本周若Top 1国家订单增速放缓但询盘量上升,可能是竞品截流信号,需立即调整报价或服务策略。'
+    )
+    items.append({'title': '🏁 竞争格局判断', 'content': competition})
+
+    # ⑤ 国家生命周期策略
+    if len(sorted_countries) >= 2:
+        mid = sorted_countries[len(sorted_countries)//2][1] if sorted_countries else 0
+        mature = [c for c, v in sorted_countries if v >= mid * 1.2]
+        emerging = [c for c, v in sorted_countries if v < mid * 0.8]
+        lifecycle = (
+            f'成熟市场({"、".join(mature[:3])})订单稳定,建议主推高毛利车型和金融服务提升客单价;'
+            f'新兴市场({"、".join(emerging[:3])})处于首单突破期,当前应以保交付口碑为核心,'
+            f'建立标杆案例后通过客户转介绍实现低成本获客。两种市场的KPI应差异化设置。'
+        )
+    else:
+        lifecycle = (
+            f'当前国家数量不足以划分生命周期梯队。'
+            f'建议积累更多国家数据后,按"订单量+复购率"双维度将国家分为导入期、成长期、成熟期、衰退期,'
+            f'匹配差异化的产品、定价和服务策略。'
+        )
+    items.append({'title': '📈 国家生命周期策略', 'content': lifecycle})
+
+    return items
+
+
+
+def _insight_weekly_team(metrics: dict, context: dict) -> list[dict]:
+    """Page 7: 人均产出趋势、区域专注度、协作效率"""
+    items = []
+    team = metrics.get('team', {})
+    team_wow = metrics.get('team_wow', {})
+    per_capita_orders = metrics.get('per_capita_orders', 0)
+    prev_per_capita = context.get('prev_per_capita_orders', 0)
+    countries = metrics.get('countries', 0)
+
+    if isinstance(team, dict) and 'owners' in team:
+        owners = team['owners']
+        qty_map = team.get('qty', {})
+    else:
+        owners = {k: v.get('orders', 0) for k, v in team.items()} if team else {}
+        qty_map = {k: v.get('qty', 0) for k, v in team.items()} if team else {}
+
+    if not owners:
+        return [
+            {'title': '💡 人均产出趋势', 'content': '本周暂无负责人数据。建议完善订单归属字段,以便评估团队人效并优化负载分配。'},
+            {'title': '⚖️ 区域专注度', 'content': '缺乏团队数据,无法评估区域专注度。建议建立人均覆盖国家数指标,超过8个时触发精力分散预警。'},
+        ]
+
+    n_members = len(owners)
+    total_orders = sum(owners.values())
+    sorted_owners = sorted(owners.items(), key=lambda x: x[1], reverse=True)
+    top_owner, top_val = sorted_owners[0]
+
+    # ① 人均产出趋势
+    pc_chg = _pct_change(per_capita_orders, prev_per_capita)
+    productivity = (
+        f'本周团队{n_members}人,人均{per_capita_orders:.1f}单,'
+        f'{"较上周" + _fmt_chg_dir(pc_chg) + _fmt_pct(pc_chg) if prev_per_capita else "基准待建立"}。'
+        f'{"人均产出提升,团队效率改善" if (pc_chg or 0) > 0 else "人均产出下滑,需排查是人员增加稀释还是订单总量下降"}。'
+        f'建议将人均产出纳入周会Review,连续两周下滑时启动专项诊断。'
+    )
+    items.append({'title': '💡 人均产出趋势', 'content': productivity})
+
+    # ② 头部与尾部差距
+    tail_vals = [v for _, v in sorted_owners[-3:]]
+    tail_avg = sum(tail_vals) / len(tail_vals) if tail_vals else 0
+    gap = _safe_div(top_val, tail_avg) if tail_avg else 0
+    disparity = (
+        f'团队头部{top_owner}本周{top_val}单,尾部3人均约{tail_avg:.1f}单,'
+        f'头尾差距{gap:.1f}倍,{"差距较大,存在激励或能力分化" if gap > 3 else "差距适中,团队相对均衡"}。'
+        f'若差距来自能力差异,建议安排头部带教;若来自资源分配不均(如国家/客户分配),'
+        f'建议重新盘点客户池,向低负载负责人倾斜高潜客户。'
+    )
+    items.append({'title': '📊 头部与尾部差距', 'content': disparity})
+
+    # ③ 区域专注度
+    if countries and n_members:
+        avg_countries = countries / n_members
+        focus = (
+            f'团队人均覆盖{avg_countries:.1f}个国家,'
+            f'{"超过8个,精力高度分散,单国深度不足" if avg_countries > 8 else "在合理范围内,兼顾广度与深度"}。'
+            f'{"建议将国家按优先级分为A/B/C类,A类由专人深耕,B类共享覆盖,C类交给代理商" if avg_countries > 8 else ""}'
+            f'{"建议对重点国家实施双人backup机制,避免单点依赖" if avg_countries <= 5 else ""}'
+            f'。国家覆盖数与订单量呈倒U型关系,过多或过少均不利。'
+        )
+    else:
+        focus = (
+            f'缺乏国家数或团队人数数据,无法计算人均覆盖。'
+            f'建议建立"人均覆盖国家数"指标,警戒线为8个,超过时触发区域重新划分流程。'
+        )
+    items.append({'title': '⚖️ 区域专注度', 'content': focus})
+
+    # ④ 协作效率
+    cross_orders = context.get('cross_owner_orders', 0)
+    if cross_orders:
+        ratio = round(cross_orders / total_orders * 100, 1) if total_orders else 0
+        collaboration = (
+            f'本周跨负责人协作订单{cross_orders}单,占总量{ratio}%。'
+            f'{"协作占比偏低,团队呈单兵作战状态,知识共享不足" if ratio < 5 else "协作机制运行良好,团队具备协同作战能力"}。'
+            f'建议对大型项目(单均大于50台)强制要求双负责人制,既分散风险又促进经验交流。'
+        )
+    else:
+        collaboration = (
+            f'本周暂无跨负责人协作订单数据。建议建立协作订单标记机制,'
+            f'识别需要多区域/多客户类型协同的复杂项目。'
+            f'协作效率是团队从"单兵"向"集团军"转型的关键指标,应纳入季度考核。'
+        )
+    items.append({'title': '🤝 协作效率', 'content': collaboration})
+
+    # ⑤ 人员稳定性
+    if team_wow:
+        churn = [o for o, d in team_wow.items() if d.get('previous', 0) > 0 and d.get('current', 0) == 0]
+        new_rise = [o for o, d in team_wow.items() if d.get('previous', 0) == 0 and d.get('current', 0) > 0]
+        stability = (
+            f'团队变动:{"上周有产出但本周挂零:" + "、".join(churn[:2]) if churn else "无流失风险信号"};'
+            f'{"本周新涌现:" + "、".join(new_rise[:2]) if new_rise else "无新人冒头"}。'
+            f'{"需关注挂零负责人的客户状态,是否离职/调岗/休假导致订单断档,必要时启动客户交接保护机制" if churn else "团队人员稳定,无异常流失或新增涌现,建议保持当前激励机制并关注长期职业发展路径"}'
+            f'{"新涌现负责人值得复盘其成功经验,快速复制至团队" if new_rise else ""}'
+        )
+    else:
+        stability = (
+            f'缺乏上周团队对比数据,无法评估人员稳定性。'
+            f'建议建立人员周环比追踪,连续两周挂零或大幅波动时触发主管一对一沟通。'
+            f'人员稳定性是业务连续性的基础,突然变动往往导致客户流失和知识断层。'
+        )
+    items.append({'title': '👥 人员稳定性', 'content': stability})
+
+    return items
+
+
+
+def _insight_weekly_issue(metrics: dict, context: dict) -> list[dict]:
+    """Page 8: 问题根因分类、影响面量化、重复性问题、解决进度"""
+    items = []
+    issues = metrics.get('issues', [])
+    support_categories = metrics.get('support_categories', {})
+    tracking_orders = metrics.get('tracking_orders', 0)
+    total_qty = metrics.get('total_qty', 0)
+
+    # ① 问题根因分类
+    if issues:
+        root_causes = {'技术': 0, '流程': 0, '外部': 0}
+        for issue in issues:
+            detail = issue.get('detail', '') + issue.get('title', '')
+            if any(k in detail for k in ['系统', 'IT', '技术', '质量', '检测']):
+                root_causes['技术'] += 1
+            elif any(k in detail for k in ['流程', '审批', '协调', '内部']):
+                root_causes['流程'] += 1
+            else:
+                root_causes['外部'] += 1
+        dominant = max(root_causes.items(), key=lambda x: x[1])
+        root = (
+            f'本周{len(issues)}项问题按根因分类:技术类{root_causes["技术"]}项、流程类{root_causes["流程"]}项、外部类{root_causes["外部"]}项。'
+            f'{"技术类最多,说明产品质量或系统稳定性存在短板,需产研部门介入" if dominant[0] == "技术" else ""}'
+            f'{"流程类最多,说明内部协作存在断点,建议梳理跨部门SOP" if dominant[0] == "流程" else ""}'
+            f'{"外部类最多,说明问题主要来自客户/政策/物流等不可控因素,需建立预案库" if dominant[0] == "外部" else ""}'
+            f'。根因分类是预防性管理的基础,建议每次问题上报时强制选择根因类型。'
+        )
+    else:
+        root = (
+            f'本周暂无显式问题记录。建议建立"无问题也是一种信号"的视角:'
+            f'若支持需求总量上升但问题记录为零,可能是问题未被识别或归类。'
+            f'建议每周五由负责人提交本周卡点,即使已自行解决也记录备案,积累组织知识。'
+        )
+    items.append({'title': '🔍 问题根因分类', 'content': root})
+
+    # ② 影响面量化
+    if issues:
+        est_orders = max(len(issues), sum(1 for i in issues if i.get('severity') == '高'))
+        est_qty = est_orders * 30
+        est_amount = est_orders * 150
+        impact = (
+            f'本周问题预计影响订单{est_orders}单、车辆约{est_qty}台、金额约¥{est_amount}万。'
+            f'其中高严重度问题{"占比高,需立即升级至管理层" if est_orders > 3 else "可控,按常规流程处理即可"}。'
+            f'建议建立"问题影响面"必填字段:阻断单数、预计台数、预计金额,便于后续按损失金额排序处理优先级。'
+        )
+    else:
+        impact = (
+            f'本周无显式问题,预计影响面为零。建议将节省的管理精力转向'
+            f'"支持需求前置化处理",把潜在问题消灭在萌芽阶段。'
+            f'支持需求中若出现高频关键词,应视为早期预警信号。'
+        )
+    items.append({'title': '💰 影响面量化', 'content': impact})
+
+    # ③ 重复性问题识别
+    if support_categories:
+        top_cat = max(support_categories.items(), key=lambda x: x[1])
+        recurring = (
+            f'本周支持需求中"{top_cat[0]}"类出现{top_cat[1]}次,为最高频类别。'
+            f'若该类别连续两周及以上位居榜首,则可判定为重复性问题。'
+            f'建议对{top_cat[0]}类问题启动"根治计划":统计发生场景→提炼标准化处理模板→培训一线人员→设置系统校验规则,'
+            f'目标是将该类问题发生频率压降50%以上。'
+        )
+    else:
+        recurring = (
+            f'本周无支持需求分类数据,无法识别重复性问题。'
+            f'建议积累4周以上数据后,按"月度出现频次大于3次且连续两周上榜"定义重复性问题,'
+            f'并建立专项改进小组负责根治。'
+        )
+    items.append({'title': '🔄 重复性问题识别', 'content': recurring})
+
+    # ④ 上周问题解决率
+    prev_resolved = context.get('prev_week_resolved_issues', 0)
+    prev_total = context.get('prev_week_total_issues', 0)
+    if prev_total:
+        resolve_rate = round(prev_resolved / prev_total * 100, 1)
+        resolution = (
+            f'上周问题{prev_total}项,已解决{prev_resolved}项,解决率{resolve_rate}%。'
+            f'{"解决率大于80%,问题闭环效率高" if resolve_rate > 80 else "解决率小于80%,存在遗留问题堆积风险"}。'
+            f'未解决问题应滚动至本周继续跟踪,避免问题"报而不决"形成管理债务。'
+            f'建议建立问题看板,按"待处理/处理中/待验证/已关闭"四状态管理。'
+        )
+    else:
+        resolution = (
+            f'缺乏上周问题解决率数据。建议建立问题生命周期管理:从上报到关闭全流程记录,'
+            f'每周计算"本周关闭率"和"平均关闭时长"。目标:关闭率大于85%、平均时长小于5个工作日。'
+        )
+    items.append({'title': '✅ 上周问题解决率', 'content': resolution})
+
+    # ⑤ 问题预防机制
+    prevention = (
+        f'建议建立三层预防机制:第一层"系统校验"(如合同超14天自动标红)、'
+        f'第二层"流程卡点"(如大额订单必须经法务预审核)、'
+        f'第三层"文化驱动"(如每月评选"零问题周"并给予团队奖励)。'
+        f'从"救火"转向"防火",是问题管理的终极目标。'
+    )
+    items.append({'title': '🛡️ 问题预防机制', 'content': prevention})
+
+    return items
+
+
+
+def _insight_weekly_plan(metrics: dict, context: dict) -> list[dict]:
+    """Page 9: 目标拆解逻辑、资源匹配、里程碑、风险对冲"""
+    items = []
+    next_week_goals = metrics.get('next_week_goals', [])
+    monthly_target = context.get('monthly_target', 0)
+    total_qty = metrics.get('total_qty', 0)
+    tracking_orders = metrics.get('tracking_orders', 0)
+
+    # ① 目标拆解逻辑
+    if next_week_goals and monthly_target:
+        total_goal = sum(g.get('number', 0) for g in next_week_goals)
+        breakdown = (
+            f'下周目标合计{total_goal}项任务,源自月度目标{monthly_target}台的四分之一拆解({monthly_target/4:.0f}台/周)。'
+            f'本周实际完成{total_qty}台,{"高于" if total_qty > monthly_target/4 else "低于"}周均目标,'
+            f'下周需{"保持惯性" if total_qty > monthly_target/4 else "加速追赶"}。'
+            f'目标拆解应遵循"存量转化保底+新增拓展增量"双轨逻辑,避免仅靠单一来源支撑。'
+        )
+    else:
+        breakdown = (
+            f'下周目标共{len(next_week_goals)}项,因缺乏月度目标或本周台数数据,暂无法做比例拆解。'
+            f'建议业务部门输入月度目标后,系统自动按"四周均衡"或"前低后高冲刺"模式生成周目标。'
+            f'当前可先基于本周{tracking_orders}单设定下周增长5%-10%的软目标。'
+        )
+    items.append({'title': '🎯 目标拆解逻辑', 'content': breakdown})
+
+    # ② 资源匹配
+    n_goals = len(next_week_goals)
+    n_owners = len(metrics.get('team', {}).get('owners', {})) if isinstance(metrics.get('team'), dict) and 'owners' in metrics.get('team', {}) else len(metrics.get('team', {}))
+    pending_ship = metrics.get('pending_shipment', 0)
+    pending_pay = metrics.get('pending_payment', 0)
+    resource = (
+        f'下周需完成{n_goals}项目标,当前团队{n_owners}人,人均承担{n_goals/n_owners if n_owners else 0:.1f}项目标。'
+        f'待发运{pending_ship}单、待收款{pending_pay}单为资源消耗大头,需提前协调物流舱位和财务催收人力。'
+        f'若目标增量超过团队当前负载20%,建议申请临时支援或外包非核心环节。'
+    )
+    items.append({'title': '⚙️ 资源匹配', 'content': resource})
+
+    # ③ 里程碑
+    milestones = []
+    for g in next_week_goals[:2]:
+        milestones.append(f'{g.get("id", "")}:{g.get("title", "")}({g.get("number", 0)})')
+    milestone_text = (
+        f'下周必须达成节点:{";".join(milestones) if milestones else "暂无具体里程碑"}。'
+        f'里程碑应满足SMART原则:具体到单数/台数、可量化、可验证。'
+        f'建议每周一晨会公开承诺里程碑,周五复盘完成率,未完成项需在当日24:00前提交原因和改进措施。'
+    )
+    items.append({'title': '🚩 里程碑节点', 'content': milestone_text})
+
+    # ④ 风险对冲(Plan B)
+    risks = context.get('next_week_risks', [])
+    if risks:
+        plan_b = (
+            f'下周已识别风险:{"、".join([r.get("title", "") for r in risks[:3]])}。'
+            f'Plan B原则:若A方案因外部因素受阻,48h内切换至备用方案。'
+            f'例如:若主船期延误,提前锁定备用船公司;若大客户延迟下单,启动备选客户清单。'
+            f'建议每项高风险目标均配置Plan B,并在周初完成资源预置。'
+        )
+    else:
+        plan_b = (
+            f'下周风险清单尚未建立。建议基于本周问题和支持需求,推演下周可能出现的3个最大风险场景:'
+            f'(1)物流延误导致E阶段积压、(2)大客户账期延迟导致D→E转化受阻、(3)政策变动导致A阶段合同搁置。'
+            f'针对每类场景提前设计应对预案,确保目标韧性。'
+        )
+    items.append({'title': '🛡️ 风险对冲(Plan B)', 'content': plan_b})
+
+    # ⑤ 关键依赖项
+    dependencies = context.get('next_week_dependencies', [])
+    if dependencies:
+        dep_text = (
+            f'下周目标达成依赖于:{"、".join(dependencies[:3])}。'
+            f'关键依赖应提前一周确认状态,避免临时发现资源不到位导致目标悬空。'
+            f'建议建立"依赖项红绿灯"机制:周一全绿确认、周三黄灯预警、周五必须全绿放行。'
+        )
+    else:
+        dep_text = (
+            f'下周关键依赖项尚未明确。建议梳理:物流舱位确认书、财务收款截止时间、'
+            f'生产排期锁定函等关键外部确认,作为目标达成的先决条件。'
+            f'内部依赖(如跨部门协作)也应落实到具体责任人和交付时间。'
+        )
+    items.append({'title': '🔗 关键依赖项', 'content': dep_text})
+
+    return items
+
+
+
+# =============================================================================
+# MONTHLY INSIGHTS
+# =============================================================================
+
+def monthly_insights(page_type: str, metrics: dict, context: dict) -> list[dict]:
+    """Dispatch to specific monthly page insight functions."""
+    dispatch = {
+        'monthly_overview': _insight_monthly_overview,
+        'monthly_funnel': _insight_monthly_funnel,
+        'monthly_region': _insight_monthly_region,
+        'monthly_country': _insight_monthly_country,
+        'monthly_trend': _insight_monthly_trend,
+        'monthly_team': _insight_monthly_team,
+        'monthly_support': _insight_monthly_support,
+        'monthly_plan': _insight_monthly_plan,
+    }
+    fn = dispatch.get(page_type)
+    return fn(metrics, context) if fn else []
+
+
+def _insight_monthly_overview(metrics: dict, context: dict) -> list[dict]:
+    """Page 3: 月度节奏、目标达成率、季节性、年度进度"""
+    items = []
+    total_contracts = metrics.get('total_contracts', 0)
+    total_qty = metrics.get('total_qty', 0)
+    shipped_orders = metrics.get('shipped_orders', 0)
+    shipped_qty = metrics.get('shipped_qty', 0)
+    trend_by_period = metrics.get('trend_by_period', {})
+    monthly_target = context.get('monthly_target', 0)
+    yoy_total_qty = metrics.get('yoy_total_qty', 0)
+    annual_target = context.get('annual_target', 0)
+
+    # ① 月度节奏(上中下旬)
+    if trend_by_period:
+        early = trend_by_period.get('early', 0)
+        mid = trend_by_period.get('mid', 0)
+        late = trend_by_period.get('late', 0)
+        total_period = early + mid + late
+        if total_period:
+            early_pct = round(early / total_period * 100, 1)
+            mid_pct = round(mid / total_period * 100, 1)
+            late_pct = round(late / total_period * 100, 1)
+            rhythm = (
+                f'本月订单节奏:上旬{early_pct}%({early:.1f}单/日)、中旬{mid_pct}%({mid:.1f}单/日)、下旬{late_pct}%({late:.1f}单/日)。'
+                f'{"下旬冲刺特征明显,说明团队具备收官能力但前期储备不足" if late > early and late > mid else ""}'
+                f'{"上旬开门红,中下旬平稳,月度节奏健康" if early >= mid and mid >= late * 0.8 else ""}'
+                f'{"中旬平台期过长,存在阶段性懈怠,需加强月中跟踪" if mid < early * 0.7 else ""}'
+                f'。理想节奏为上旬35%-中旬30%-下旬35%,当前{"接近理想" if 25 <= early_pct <= 45 and 25 <= mid_pct <= 40 else "需调整"}。'
+            )
+        else:
+            rhythm = f'本月分旬数据异常,无法计算节奏占比。建议检查日报数据完整性。'
+    else:
+        rhythm = (
+            f'本月累计{total_contracts}单,缺乏上中下旬分旬数据。'
+            f'建议将月度按自然旬拆分,识别"月初开门红/月中平台期/月末冲刺"的典型模式。'
+            f'节奏分析是预测下月走势和优化资源投放时点的重要依据。'
+        )
+    items.append({'title': '📅 月度节奏分析', 'content': rhythm})
+
+    # ② 目标达成率
+    if monthly_target and total_qty:
+        achievement = round(total_qty / monthly_target * 100, 1)
+        gap = total_qty - monthly_target
+        target_text = (
+            f'本月实际完成{total_qty}台,目标{monthly_target}台,达成率{achievement}%,'
+            f'{"超额" if gap >= 0 else "缺口"}{abs(gap)}台。'
+            f'{"达成率超过100%,建议复盘成功因素并固化为标准打法" if achievement >= 100 else ""}'
+            f'{"达成率90%-100%,基本达标但无冗余,需确保最后几天无退单" if 90 <= achievement < 100 else ""}'
+            f'{"达成率低于90%,缺口较大,需启动紧急补单或下调下月目标" if achievement < 90 else ""}'
+            f'。目标达成率应分解到周,每周偏差大于10%时即触发预警。'
+        )
+    else:
+        target_text = (
+            f'本月完成{total_qty}台,因未设定月度目标,无法计算达成率。'
+            f'建议业务部门在月初输入目标值,系统将自动按周拆解并生成进度看板。'
+            f'当前可先以历史月均值为隐性目标,评估相对表现。'
+        )
+    items.append({'title': '🎯 目标达成率', 'content': target_text})
+
+    # ③ 季节性分析(同比)
+    if yoy_total_qty:
+        yoy_chg = _pct_change(total_qty, yoy_total_qty)
+        seasonal = (
+            f'本月{total_qty}台,去年同期{yoy_total_qty}台,同比{_fmt_chg_dir(yoy_chg)}{_fmt_pct(yoy_chg)}。'
+            f'{"同比加速增长,市场景气度上行或我司市占率提升" if (yoy_chg or 0) > 10 else ""}'
+            f'{"同比小幅增长,与行业大盘同步" if 0 <= (yoy_chg or 0) <= 10 else ""}'
+            f'{"同比下滑,需警惕行业下行或竞争加剧" if (yoy_chg or 0) < 0 else ""}'
+            f'。建议结合行业报告判断:若我司增速高于行业,说明策略有效;若低于行业,则需重新审视产品/定价/渠道。'
+        )
+    else:
+        seasonal = (
+            f'本月完成{total_qty}台,缺乏去年同期数据。'
+            f'建议建立年度数据档案,以便进行同比分析。同比是剔除季节性干扰的最佳方式,'
+            f'尤其在受春节、海外斋月等影响的月份,环比可能失真,同比更具参考价值。'
+        )
+    items.append({'title': '📊 季节性分析(同比)', 'content': seasonal})
+
+    # ④ 年度进度
+    if annual_target and total_qty:
+        annual_progress = round(total_qty / annual_target * 100, 1)
+        month = context.get('month', 1)
+        expected_annual = month / 12 * 100
+        annual_gap = annual_progress - expected_annual
+        annual_text = (
+            f'本月贡献{total_qty}台,占年度目标{annual_target}台的{annual_progress}%,'
+            f'按时间进度应达{expected_annual:.1f}%,{"领先" if annual_gap >= 0 else "落后"}{abs(annual_gap):.1f}个百分点。'
+            f'{"年度进度超前,建议适当储备Q4项目避免年末透支" if annual_gap > 5 else ""}'
+            f'{"年度进度滞后,剩余月份需月均追赶" + str(abs(int(annual_gap/100*annual_target/(12-month)))) + "台" if annual_gap < -5 and month < 12 else ""}'
+            f'。年度进度是战略资源配置的北极星指标,每季度应做一次资源再平衡。'
+        )
+    else:
+        annual_text = (
+            f'本月完成{total_qty}台,因未设定年度目标,无法计算年度进度。'
+            f'建议年初输入年度目标,系统自动按月分解并生成进度条。'
+            f'年度目标的合理性直接影响月度策略:过高导致团队透支,过低导致资源闲置。'
+        )
+    items.append({'title': '🗓️ 年度进度', 'content': annual_text})
+
+    # ⑤ 交付闭环效率
+    if total_contracts:
+        ship_pct = round(shipped_orders / total_contracts * 100, 1)
+        delivery = (
+            f'本月已发运{shipped_orders}单({shipped_qty}台),占月内合同{ship_pct}%。'
+            f'{"发运占比高,交付闭环效率高" if ship_pct > 60 else "发运占比偏低,大量订单滞留中后期阶段"}。'
+            f'需区分"本月新签本月发运"(效率极高)与"历史存量本月发运"(清理旧账),'
+            f'前者反映流程效率,后者反映历史包袱。建议分别统计两类占比。'
+        )
+    else:
+        delivery = (
+            f'本月暂无合同数据,无法计算交付闭环效率。'
+            f'建议建立"当月签约-当月发运"比率作为流程效率核心指标,目标值大于30%。'
+        )
+    items.append({'title': '🚢 交付闭环效率', 'content': delivery})
+
+    return items
+
+
+
+def _insight_monthly_funnel(metrics: dict, context: dict) -> list[dict]:
+    """Page 4: 各环节转化率、瓶颈诊断、历史最优、改进路线图"""
+    items = []
+    status_funnel = metrics.get('status_funnel', {})
+    stage_analysis = metrics.get('stage_analysis', {})
+    total_contracts = metrics.get('total_contracts', 0)
+
+    if not status_funnel or not total_contracts:
+        return [
+            {'title': '💡 漏斗结构诊断', 'content': '本月暂无漏斗数据。建议完善订单状态字段,以便计算各阶段转化率并识别瓶颈。'},
+        ]
+
+    # ① 各环节转化率分析
+    names = ['合同拟定中', '已锁定合同待付订金', '已付订金待生产', '已生产待付尾款', '已付尾款待发运', '已发运']
+    prev_funnel = context.get('prev_status_funnel', {})
+    conv_text = '本月漏斗各阶段占比:'
+    parts = []
+    for name in names:
+        pct = status_funnel.get(name, {}).get('pct', 0)
+        prev_pct = prev_funnel.get(name, {}).get('pct', 0) if prev_funnel else 0
+        chg = _pct_change(pct, prev_pct)
+        parts.append(f'{name}{pct}%({_fmt_chg_dir(chg)}{_fmt_pct(chg)})')
+    conv_text += '、'.join(parts[:4]) + '。'
+    conv_text += '转化率变化反映流程效率波动,建议锁定两个关键转化节点重点监控:A→B(合同锁定)和D→E(尾款回收)。'
+    items.append({'title': '💡 各环节转化率分析', 'content': conv_text})
+
+    # ② 瓶颈环节深度诊断
+    early = stage_analysis.get('early', {}).get('pct', 0)
+    mid = stage_analysis.get('mid', {}).get('pct', 0)
+    late = stage_analysis.get('late', {}).get('pct', 0)
+    bottleneck = (
+        f'阶段结构:前期{early}%(合同+锁定)、中期{mid}%(生产)、后期{late}%(待发运+已发运)。'
+        f'{"前期占比过高,新增pipeline充足但转化效率低,需重点突破合同审批和定金催收" if early > 40 else ""}'
+        f'{"中期占比过高,生产端积压严重,需排查产能瓶颈或生产计划失衡" if mid > 40 else ""}'
+        f'{"后期占比过高,交付压力大,需确保物流和报关资源到位" if late > 40 else ""}'
+        f'。与行业benchmark对比:理想状态为前期30%-中期35%-后期35%,当前{"接近理想" if 25 <= early <= 40 and 25 <= mid <= 45 else "偏离理想,需专项改进"}。'
+    )
+    items.append({'title': '📉 瓶颈环节深度诊断', 'content': bottleneck})
+
+    # ③ 历史最优水平对比
+    hist_best = context.get('hist_best_conversion', {})
+    if hist_best:
+        best_text = (
+            f'历史最优A→B转化率{hist_best.get("a_to_b", "—")}%、D→E转化率{hist_best.get("d_to_e", "—")}%。'
+            f'本月表现与最优水平对比,可量化当前流程效率损失。'
+            f'差距大于10个百分点时,说明流程存在显著退化,需启动流程再造;'
+            f'差距小于5个百分点时,可通过微调优化逼近最优。建议每季度更新历史最优基准。'
+        )
+    else:
+        best_text = (
+            f'本月A→B及D→E转化率数据已记录,但历史最优基准尚未建立。'
+            f'建议追溯过去12个月数据,提取各阶段转化率峰值作为"历史最优"标杆。'
+            f'历史最优不仅是目标,更是证明"我们曾做到过"的证据,对团队信心建设至关重要。'
+        )
+    items.append({'title': '🏆 历史最优水平对比', 'content': best_text})
+
+    # ④ 改进路线图
+    roadmap = (
+        f'下月漏斗改进重点:'
+        f'(1)A→B转化:优化合同模板,将标准合同审批周期从5天压缩至3天;'
+        f'(2)C→D转化:建立生产进度可视化看板,让客户实时追踪车辆生产状态,减少催单焦虑;'
+        f'(3)D→E转化:财务前置介入,车辆下线前7天启动尾款提醒,而非传统下线后催收。'
+        f'每项改进需设定量化目标、责任人和验收标准,月底复盘执行效果。'
+    )
+    items.append({'title': '🛣️ 改进路线图', 'content': roadmap})
+
+    # ⑤ 资金占用与周转
+    pending_pay = metrics.get('pending_payment', {}).get('orders', 0)
+    pending_pay_qty = metrics.get('pending_payment', {}).get('qty', 0)
+    pending_ship = metrics.get('pending_shipment', {}).get('orders', 0)
+    capital = (
+        f'本月D阶段(已生产待付尾款){pending_pay}单(约{pending_pay_qty}台),是资金占用核心环节。'
+        f'假设单台均价15万,则D阶段资金占用约¥{pending_pay_qty * 15:,}万。'
+        f'若D阶段平均滞留天数超过14天,建议对超期订单启动"财务+销售"联合催收机制,'
+        f'目标是将D阶段平均周转天数压降至10天以内。'
+    )
+    items.append({'title': '💰 资金占用与周转', 'content': capital})
+
+    return items
+
+
+
+def _insight_monthly_region(metrics: dict, context: dict) -> list[dict]:
+    """Page 5: 区域战略优先级矩阵、资源投入ROI、订单结构差异"""
+    items = []
+    region_dist = metrics.get('region_dist', {})
+    prev_region_dist = context.get('prev_region_dist', {})
+
+    if not region_dist:
+        return [
+            {'title': '💡 区域战略优先级矩阵', 'content': '本月暂无区域分布数据。建议完善国家-区域映射表,以便按区域维度分析市场规模与增速矩阵。'},
+        ]
+
+    # ① 区域战略优先级矩阵(市场规模×增速)
+    regions = []
+    for name, data in region_dist.items():
+        qty = data.get('qty', 0)
+        pct = data.get('pct', 0)
+        prev_qty = prev_region_dist.get(name, {}).get('qty', 0) if prev_region_dist else 0
+        growth = _pct_change(qty, prev_qty)
+        regions.append((name, qty, pct, growth))
+
+    stars = [r for r in regions if r[2] > 20 and (r[3] is None or r[3] > 0)]
+    cows = [r for r in regions if r[2] > 20 and (r[3] is not None and r[3] <= 0)]
+    questions = [r for r in regions if r[2] <= 20 and (r[3] is None or r[3] > 0)]
+    dogs = [r for r in regions if r[2] <= 20 and (r[3] is not None and r[3] <= 0)]
+
+    matrix = (
+        f'基于"市场规模×增速"四象限分析:'
+        f'{"明星(高规模+高增长):" + "、".join([r[0] for r in stars[:2]]) + ",应追加资源扩大优势;" if stars else ""}'
+        f'{"现金牛(高规模+低增长):" + "、".join([r[0] for r in cows[:2]]) + ",维持投入收割利润;" if cows else ""}'
+        f'{"问题(低规模+高增长):" + "、".join([r[0] for r in questions[:2]]) + ",需判断是真潜力还是虚假繁荣;" if questions else ""}'
+        f'{"瘦狗(低规模+低增长):" + "、".join([r[0] for r in dogs[:2]]) + ",收缩资源或调整模式。" if dogs else ""}'
+    )
+    items.append({'title': '💡 区域战略优先级矩阵', 'content': matrix})
+
+    # ② 资源投入ROI
+    roi_text = (
+        f'区域ROI评估应超越订单量,引入利润率、售后成本、物流复杂度三维度。'
+        f'例如:某区域订单量大但物流成本占比高(偏远国/内陆国),其真实ROI可能低于订单量小的近海区域。'
+        f'建议每季度输出区域损益表,按"净利润=订单毛利-物流成本-售后成本-人力分摊"公式计算,'
+        f'将资源向高ROI区域倾斜,低ROI区域探索代理商模式降低成本。'
+    )
+    items.append({'title': '💰 资源投入ROI', 'content': roi_text})
+
+    # ③ 区域间订单结构差异
+    if len(regions) >= 2:
+        r1_name, r1_data = max(region_dist.items(), key=lambda x: x[1].get('qty', 0))
+        r1_top = [c['country'] for c in r1_data.get('top_countries', [])[:2]]
+        structure = (
+            f'{r1_name}为最大区域,其Top国家{r1_top}的车型偏好可能与其他区域存在显著差异。'
+            f'例如:亚洲市场偏好紧凑型电动车,非洲市场偏好皮卡/商用车,拉美市场对SUV需求旺盛。'
+            f'区域间车型差异反映市场需求本质不同,建议按区域定制产品组合和营销话术,'
+            f'避免全球统一策略导致的水土不服。'
+        )
+    else:
+        structure = (
+            f'本月区域数据不足,无法分析订单结构差异。'
+            f'建议积累多区域数据后,按"区域×车型"交叉分析,识别区域专属需求特征。'
+        )
+    items.append({'title': '📊 区域间订单结构差异', 'content': structure})
+
+    # ④ 区域培育策略
+    if questions:
+        q_names = [r[0] for r in questions[:2]]
+        nurture = (
+            f'问题区域({"、".join(q_names)})增速快但规模小,处于市场培育期。'
+            f'培育策略:前期以"样板工程"为核心,投入1-2个标杆客户确保极致交付体验;'
+            f'中期通过客户转介绍和本地车展扩大知名度;后期引入本地代理商实现轻资产扩张。'
+            f'培育期通常需要6-12个月,需设定阶段性里程碑避免过早放弃。'
+        )
+    else:
+        nurture = (
+            f'本月暂无高增长低基数的问题区域。建议审视现有区域增速:'
+            f'若有区域增速超过50%但占比仍低于10%,应立即纳入问题区域清单并给予专项资源。'
+        )
+    items.append({'title': '🌱 区域培育策略', 'content': nurture})
+
+    return items
+
+
+
+def _insight_monthly_country(metrics: dict, context: dict) -> list[dict]:
+    """Page 6: 国家组合健康度、大客户集中度、新国家孵化、竞争态势"""
+    items = []
+    top_countries = metrics.get('top_countries', {})
+    top_countries_change = metrics.get('top_countries_change', {})
+    total_qty = metrics.get('total_qty', 0)
+
+    if not top_countries:
+        return [
+            {'title': '💡 国家组合健康度', 'content': '本月暂无国家分布数据。建议完善目的国家字段,以便评估国家组合健康度并制定分散策略。'},
+        ]
+
+    country_qty = {}
+    country_orders = {}
+    for c, v in top_countries.items():
+        if isinstance(v, int):
+            country_qty[c] = v
+            country_orders[c] = 0
+        else:
+            country_qty[c] = v.get('qty', 0)
+            country_orders[c] = v.get('orders', 0)
+
+    sorted_countries = sorted(country_qty.items(), key=lambda x: x[1], reverse=True)
+    top3_qty = sum(v for _, v in sorted_countries[:3])
+    top3_pct = round(top3_qty / total_qty * 100, 1) if total_qty else 0
+
+    # ① 国家组合健康度评分
+    health_score = 100
+    if top3_pct > 50:
+        health_score -= 30
+    elif top3_pct > 40:
+        health_score -= 15
+    if len(sorted_countries) < 5:
+        health_score -= 20
+    health = (
+        f'国家组合健康度评分{health_score}/100(Top 3集中度{top3_pct}%、覆盖国家数{len(sorted_countries)})。'
+        f'{"评分优秀,国家分布均衡,抗风险能力强" if health_score >= 85 else ""}'
+        f'{"评分良好,存在优化空间,建议培育第二梯队" if 70 <= health_score < 85 else ""}'
+        f'{"评分偏低,头部依赖严重或覆盖不足,需立即启动分散计划" if health_score < 70 else ""}'
+        f'。评分规则:Top 3集中度每超10%扣15分,覆盖国家不足5个扣20分。'
+    )
+    items.append({'title': '💡 国家组合健康度评分', 'content': health})
+
+    # ② 大客户集中度风险
+    top1_country, top1_qty = sorted_countries[0]
+    top1_orders = country_orders.get(top1_country, 0)
+    risk = (
+        f'{top1_country}本月{top1_qty}台({top1_orders}单)领跑。'
+        f'若该国最大单一客户占比超过50%,则触发红色预警:该客户流失将导致月度目标缺口{round(top1_qty * 0.5)}台。'
+        f'建议对该国客户结构进行"金字塔"分层:底部散户(小于5台)占60%、腰部客户(5-20台)占30%、顶部大客户(大于20台)不超过10%,'
+        f'确保任何单一客户流失不会动摇基本盘。'
+    )
+    items.append({'title': '👤 大客户集中度风险', 'content': risk})
+
+    # ③ 新国家孵化进展
+    new_countries = context.get('new_countries_this_month', [])
+    if new_countries:
+        new_text = (
+            f'本月新开拓国家:{"、".join(new_countries[:5])}。'
+            f'新国家首单是0到1的突破,意义重大但风险较高。建议对新国家实施"护航计划":'
+            f'首单交付全程由资深负责人跟进,确保零差错;交付后30天内回访客户,收集反馈并建立口碑案例;'
+            f'若首单反馈良好,第2-3单可逐步放宽管控,交由本地负责人常规跟进。'
+        )
+    else:
+        new_text = (
+            f'本月无新国家突破。建议审视"新国家孵化 pipeline":是否有正在洽谈的潜在市场?'
+            f'若连续3个月无新国家,需反思是市场选择过于保守还是孵化流程存在断点。'
+            f'建议每季度设定"新国家突破"为团队OKR之一,激励市场拓展。'
+        )
+    items.append({'title': '🌱 新国家孵化进展', 'content': new_text})
+
+    # ④ 竞争态势
+    competitive = (
+        f'竞争态势监测建议:对Top 3国家建立"竞品对标"机制,跟踪维度包括:'
+        f'(1)价格带:竞品是否发起价格战、促销力度如何;'
+        f'(2)交付周期:竞品从签约到发运的时长是否短于我方;'
+        f'(3)服务网络:竞品是否在当地设立配件仓或服务站。'
+        f'本月若某国订单增速放缓但询盘量上升,可能是竞品截流信号,需立即调整策略。'
+    )
+    items.append({'title': '🏁 竞争态势', 'content': competitive})
+
+    return items
+
+
+
+def _insight_monthly_trend(metrics: dict, context: dict) -> list[dict]:
+    """Page 7: 阶段性特征、异常波动归因、外部因素、下月预测"""
+    items = []
+    trend_by_period = metrics.get('trend_by_period', {})
+    daily_trend = metrics.get('daily_trend', {})
+    peak_dates = metrics.get('peak_dates', [])
+    forecast_next = metrics.get('forecast_next', 0)
+    total_qty = metrics.get('total_qty', 0)
+    total_contracts = metrics.get('total_contracts', 0)
+
+    # ① 阶段性特征
+    if trend_by_period:
+        early = trend_by_period.get('early', 0)
+        mid = trend_by_period.get('mid', 0)
+        late = trend_by_period.get('late', 0)
+        late_chg = trend_by_period.get('late_change_pct')
+        phase = (
+            f'本月三旬日均:上旬{early:.1f}单、中旬{mid:.1f}单、下旬{late:.1f}单。'
+            f'{"上旬开门红,开局强势" if early > mid and early > late else ""}'
+            f'{"中旬平台期,需警惕懈怠" if mid < early * 0.8 and mid < late * 0.8 else ""}'
+            f'{"下旬冲刺,收官能力强" if late > early and late > mid else ""}'
+            f'下旬较中旬{_fmt_chg_dir(late_chg)}{_fmt_pct(late_chg)}。'
+            f'阶段性特征反映了团队的节奏控制能力,理想状态为"高开稳走",避免过度依赖月末冲刺。'
+        )
+    else:
+        phase = (
+            f'本月累计{total_contracts}单,缺乏上中下旬分旬数据。'
+            f'建议将月度按自然旬拆分,识别"月初开门红/月中平台期/月末冲刺"的典型模式。'
+            f'阶段特征分析有助于优化资源投放时点:上旬重签约、中旬重生产、下旬重交付。'
+        )
+    items.append({'title': '📅 阶段性特征', 'content': phase})
+
+    # ② 异常波动事件归因
+    if daily_trend and peak_dates:
+        peak_vals = [daily_trend.get(d, 0) for d in peak_dates]
+        avg_val = sum(daily_trend.values()) / len(daily_trend)
+        anomaly = (
+            f'本月峰值日:{"、".join(peak_dates)}({"、".join([str(v) for v in peak_vals])}单),'
+            f'分别为均值的{_safe_div(max(peak_vals), avg_val):.1f}倍。'
+            f'峰值日需区分"机会型"(大客户集中下单/展会签约)与"补录型"(历史订单系统批量导入)。'
+            f'建议对峰值日逐单标注事件类型,长期积累后可识别真正的业务脉冲与数据噪声。'
+        )
+    else:
+        anomaly = (
+            f'本月缺乏分日峰值数据,无法识别异常波动事件。'
+            f'建议建立日报机制后,采用"均值+2倍标准差"规则自动标记异常日,并要求负责人提交事件说明。'
+        )
+    items.append({'title': '⚠️ 异常波动事件归因', 'content': anomaly})
+
+    # ③ 外部因素关联
+    fx_change = context.get('fx_change_pct')
+    policy_events = context.get('policy_events', [])
+    shipping_delay = context.get('shipping_delay_days', 0)
+    external = (
+        f'外部因素评估:'
+        f'{"汇率变动" + _fmt_pct(fx_change) + ",对出口报价竞争力产生" + ("正面" if (fx_change or 0) < 0 else "负面") + "影响;" if fx_change is not None else "暂无汇率数据;"}'
+        f'{"本月政策事件:" + "、".join(policy_events[:2]) + ";" if policy_events else "暂无重大政策事件;"}'
+        f'{"船期平均延误" + str(shipping_delay) + "天,可能滞后影响客户下单信心" if shipping_delay else "船期正常"}。'
+        f'外部因素与订单波动存在1-4周滞后,建议建立外部因素登记簿,量化其对后续月份的影响。'
+    )
+    items.append({'title': '🌐 外部因素关联', 'content': external})
+
+    # ④ 下月预测
+    mom_pct = _pct_change(total_contracts, metrics.get('prev_total_contracts', 0))
+    if forecast_next:
+        forecast = (
+            f'Pipeline预测下月交付{forecast_next}台,结合本月{total_contracts}单(MoM {_fmt_pct(mom_pct)}),'
+            f'下月订单量预计维持在{int(total_contracts * 0.9)}-{int(total_contracts * 1.15)}单区间。'
+            f'{"若外部利好持续,有望突破区间上限" if (mom_pct or 0) > 10 else ""}'
+            f'{"若趋势承压,可能回落至区间下限,需提前储备补单方案" if (mom_pct or 0) < 0 else ""}'
+            f'。预测准确性应每月复盘,偏差超过20%时校准预测模型参数。'
+        )
+    else:
+        forecast = (
+            f'本月完成{total_contracts}单,缺乏pipeline预测数据。'
+            f'基于历史趋势,下月预计{int(total_contracts * 0.9)}-{int(total_contracts * 1.1)}单。'
+            f'建议建立A+B阶段存量订单与下月签约的转化模型,提升预测准确性。'
+        )
+    items.append({'title': '🔮 下月预测', 'content': forecast})
+
+    # ⑤ 趋势持续性信号
+    sustain = (
+        f'判断下月趋势持续性的三个信号:'
+        f'(1)早期pipeline:A+B阶段存量是否充足,能否支撑下月转化;'
+        f'(2)客户活跃度:本月更新进度订单数是否维持高位,反映客户 engagement;'
+        f'(3)支持需求趋势:若支持需求逐周下降,说明流程顺畅,订单转化阻力减小。'
+        f'三个信号中有两个向好,则下月趋势延续概率大于70%。'
+    )
+    items.append({'title': '📊 趋势持续性信号', 'content': sustain})
+
+    return items
+
+
+
+def _insight_monthly_team(metrics: dict, context: dict) -> list[dict]:
+    """Page 8: 人均效能、团队结构优化、激励效果、下月人员配置"""
+    items = []
+    team = metrics.get('team', {})
+    per_capita_orders = metrics.get('per_capita_orders', 0)
+    per_capita_qty = metrics.get('per_capita_qty', 0)
+    total_contracts = metrics.get('total_contracts', 0)
+    total_qty = metrics.get('total_qty', 0)
+
+    if isinstance(team, dict) and 'owners' in team:
+        owners = team['owners']
+        qty_map = team.get('qty', {})
+    else:
+        owners = {k: v.get('orders', 0) for k, v in team.items()} if team else {}
+        qty_map = {k: v.get('qty', 0) for k, v in team.items()} if team else {}
+
+    if not owners:
+        return [
+            {'title': '💡 人均效能趋势', 'content': '本月暂无负责人数据。建议完善订单归属字段,以便评估团队人效并优化配置。'},
+        ]
+
+    n_members = len(owners)
+    sorted_owners = sorted(owners.items(), key=lambda x: x[1], reverse=True)
+    top_owner, top_val = sorted_owners[0]
+    prev_per_capita = context.get('prev_per_capita_orders', 0)
+    yoy_per_capita = context.get('yoy_per_capita_orders', 0)
+
+    # ① 人均效能趋势
+    mom_chg = _pct_change(per_capita_orders, prev_per_capita)
+    yoy_chg = _pct_change(per_capita_orders, yoy_per_capita)
+    efficiency = (
+        f'本月人均{per_capita_orders:.1f}单({per_capita_qty:.0f}台),'
+        f'环比{_fmt_chg_dir(mom_chg)}{_fmt_pct(mom_chg)},同比{_fmt_chg_dir(yoy_chg)}{_fmt_pct(yoy_chg)}。'
+        f'{"人均效能双升,团队能力在积累" if (mom_chg or 0) > 0 and (yoy_chg or 0) > 0 else ""}'
+        f'{"环比升但同比降,说明短期改善但尚未恢复历史水平" if (mom_chg or 0) > 0 and (yoy_chg or 0) < 0 else ""}'
+        f'{"人均效能承压,需排查是市场总量下降还是团队效率退化" if (mom_chg or 0) < 0 and (yoy_chg or 0) < 0 else ""}'
+        f'。人均效能是团队健康度的核心指标,建议纳入月度考核并与激励挂钩。'
+    )
+    items.append({'title': '💡 人均效能趋势', 'content': efficiency})
+
+    # ② 团队结构优化建议
+    tail_count = sum(1 for _, v in sorted_owners if v < per_capita_orders * 0.5)
+    structure = (
+        f'本月团队{n_members}人,尾部{tail_count}人产出低于人均50%。'
+        f'{"尾部占比高,团队呈金字塔结构,需加强腰部建设" if tail_count > n_members // 3 else "尾部占比低,团队呈橄榄型,结构健康"}。'
+        f'优化建议:'
+        f'(1)低产出人员:分析是能力问题(培训)还是资源问题(客户/国家重新分配);'
+        f'(2)高产出人员:防止过度依赖,建立AB角备份;'
+        f'(3)新入职人员:前3个月以保护期为主,第4个月起按正常人均考核。'
+    )
+    items.append({'title': '⚖️ 团队结构优化建议', 'content': structure})
+
+    # ③ 激励效果评估
+    incentive_effect = context.get('incentive_effect', '')
+    if incentive_effect:
+        incentive = (
+            f'本月激励政策效果:{incentive_effect}。'
+            f'激励效果评估应区分"增量激励"(新签奖励)与"存量激励"(转化奖励),'
+            f'避免团队为拿新签奖而忽视存量转化。建议设置激励上限和平衡系数,'
+            f'确保短期激励与长期客户价值不冲突。'
+        )
+    else:
+        incentive = (
+            f'本月暂无激励效果量化数据。建议下月实施激励政策时,同步记录政策前后的人均产出变化。'
+            f'激励效果=(政策后人均产出-政策前人均产出)/激励总成本,ROI大于1.5说明激励有效。'
+        )
+    items.append({'title': '🏆 激励效果评估', 'content': incentive})
+
+    # ④ 标杆对比与提升路径
+    top_gap = top_val - per_capita_orders if per_capita_orders else 0
+    best_practice = (
+        f'本月团队领跑者{top_owner}完成{top_val}单,超人均{top_gap:.1f}单,是均值{_safe_div(top_val, per_capita_orders) if per_capita_orders else 0:.1f}倍。'
+        f'建议将其客户跟进SOP、谈判策略、响应时效提炼为标准流程,通过"老带新"机制复制到全团队。'
+        f'同时建立月度技能分享会,让头部负责人分享成交案例和失败教训,缩短新人成长周期。'
+        f'对于连续两月低于人均50%的成员,启动一对一辅导计划,30天内无明显改善则考虑调岗。'
+    )
+    items.append({'title': '🎯 标杆对比与提升路径', 'content': best_practice})
+
+    # ⑤ 下月人员配置
+    next_month_target = context.get('next_month_target', 0)
+    if next_month_target and per_capita_orders:
+        required = int(next_month_target / per_capita_orders)
+        gap = required - n_members
+        staffing = (
+            f'下月目标{next_month_target}单,按本月人均{per_capita_orders:.1f}单计算,需{required}人,'
+            f'当前{n_members}人,{"缺口" + str(gap) + "人,建议启动招聘或内部调配" if gap > 0 else "冗余" + str(abs(gap)) + "人,可优化至其他业务线" if gap < 0 else "刚好匹配"}。'
+            f'若市场处于上升期,建议按目标人数的110%配置,预留20%冗余应对突发需求。'
+        )
+    else:
+        staffing = (
+            f'下月人员配置建议:维持现有{n_members}人编制,重点优化结构而非单纯扩编。'
+            f'若人均产出连续两月下滑,说明市场或团队出现问题,此时扩编只会稀释效率;'
+            f'若人均产出持续上升且超负荷,则扩编是必要且紧迫的。'
+        )
+    items.append({'title': '👥 下月人员配置', 'content': staffing})
+
+    return items
+
+
+
+def _insight_monthly_support(metrics: dict, context: dict) -> list[dict]:
+    """Page 9: 需求类型趋势、高频问题根因、流程优化、SLA达成率"""
+    items = []
+    support_categories = metrics.get('support_categories', {})
+    support_count = metrics.get('support_count', 0)
+    support_pct = metrics.get('support_pct', 0)
+    prev_support_categories = context.get('prev_support_categories', {})
+
+    # ① 需求类型趋势
+    if support_categories and prev_support_categories:
+        trends = []
+        for cat, count in support_categories.items():
+            prev = prev_support_categories.get(cat, 0)
+            chg = _pct_change(count, prev)
+            trends.append((cat, chg))
+        trends.sort(key=lambda x: (x[1] or 0), reverse=True)
+        fastest = trends[0] if trends else (None, None)
+        trend_text = (
+            f'本月支持需求共{support_count}项(占订单{support_pct}%)。'
+            f'增长最快类别为"{fastest[0]}"({_fmt_pct(fastest[1])}),'
+            f'{"说明该领域存在系统性短板,需优先根治" if (fastest[1] or 0) > 50 else ""}'
+            f'{"增速可控,按常规节奏优化即可" if (fastest[1] or 0) <= 50 else ""}'
+            f'。建议每月输出支持需求趋势图,识别"慢性增长"与"急性爆发"两类问题,分别用流程优化和应急响应处理。'
+        )
+    else:
+        trend_text = (
+            f'本月支持需求共{support_count}项(占订单{support_pct}%)。'
+            f'缺乏上月分类数据,无法计算各类别增速。建议建立支持需求分类台账,'
+            f'按财务/法务/物流/售后/IT五大类归档,每月对比识别增长最快的类别。'
+        )
+    items.append({'title': '📈 需求类型趋势', 'content': trend_text})
+
+    # ② 高频问题根因
+    if support_categories:
+        top3 = sorted(support_categories.items(), key=lambda x: x[1], reverse=True)[:3]
+        root = (
+            f'本月Top 3高频问题:{"、".join([f"{c}({v}项)" for c, v in top3])}。'
+            f'高频问题的根因通常可归结为三类:'
+            f'(1)流程断点:跨部门协作无明确SLA,导致需求在部门间空转;'
+            f'(2)信息孤岛:客户/订单/物流数据分散,重复查询浪费人力;'
+            f'(3)能力短板:一线人员对产品/政策理解不足,过度依赖支持部门。'
+            f'建议对Top 3问题各做一次5Why分析,找到可系统改进的根因。'
+        )
+    else:
+        root = (
+            f'本月无支持需求分类数据。建议强制要求提交支持需求时选择类别并简述根因,'
+            f'否则不予处理。数据质量是分析的前提,没有分类的数据无法产生洞察。'
+        )
+    items.append({'title': '🔍 高频问题根因', 'content': root})
+
+    # ③ 流程优化建议
+    optimization = (
+        f'流程优化建议:'
+        f'(1)自助化:将Top 20%高频问题转化为FAQ或系统自助查询,减少人工支持需求;'
+        f'(2)前置化:在订单关键节点(如合同锁定/生产完成)自动触发检查清单,提前消除潜在问题;'
+        f'(3)标准化:对剩余80%中频问题建立SOP和处理模板,将平均处理时长压缩50%。'
+        f'优化效果应每月量化:支持需求总量是否下降、重复问题占比是否降低、客户满意度是否提升。'
+    )
+    items.append({'title': '⚙️ 流程优化建议', 'content': optimization})
+
+    # ④ SLA达成率
+    sla_data = context.get('sla_achievement', {})
+    if sla_data:
+        overall = sla_data.get('overall', 0)
+        sla_text = (
+            f'本月支持需求SLA达成率{overall}%。'
+            f'{"达成率大于90%,响应速度优秀" if overall > 90 else "达成率80%-90%,基本达标但存在超期" if overall >= 80 else "达成率低于80%,响应速度严重滞后,需立即增派人手或优化流程"}。'
+            f'分类达成率:{"、".join([f"{k}:{v}%" for k, v in sla_data.items() if k != "overall"][:3])}。'
+            f'低于目标的类别应作为下月改进重点,分配专项资源提升。'
+        )
+    else:
+        sla_text = (
+            f'本月暂无SLA达成率数据。建议为每类支持需求设定处理时限:'
+            f'紧急(4h)、高(24h)、中(48h)、低(72h),并记录实际关闭时间。'
+            f'SLA是支持部门的服务承诺,也是内部客户体验的核心指标。'
+        )
+    items.append({'title': '⏱️ SLA达成率', 'content': sla_text})
+
+    return items
+
+
+
+def _insight_monthly_plan(metrics: dict, context: dict) -> list[dict]:
+    """Page 10: 目标可行性分析、关键假设、风险场景、Contingency Plan"""
+    items = []
+    next_month_goals = metrics.get('next_month_goals', [])
+    total_qty = metrics.get('total_qty', 0)
+    risks = metrics.get('risks', [])
+    forecast_next = metrics.get('forecast_next', 0)
+
+    # ① 目标可行性分析
+    if next_month_goals:
+        goal_total = sum(g.get('number', 0) for g in next_month_goals)
+        feasibility = (
+            f'下月目标共{len(next_month_goals)}项,量化指标合计{goal_total}。'
+            f'基于本月{total_qty}台及pipeline预测{forecast_next}台,目标可行性{"高" if forecast_next >= goal_total * 0.8 else "中" if forecast_next >= goal_total * 0.5 else "低"}。'
+            f'{"Pipeline充足,目标具备充分支撑" if forecast_next >= goal_total else "Pipeline低于目标,需额外拓展新单填补缺口"}。'
+            f'可行性分析应每月更新,若连续两月可行性评级为低,需下调目标或追加资源。'
+        )
+    else:
+        feasibility = (
+            f'下月目标尚未设定。建议基于本月{total_qty}台和pipeline{forecast_next}台,'
+            f'按"保底(本月×0.9)/基准(本月×1.0)/挑战(本月×1.2)"三档设定目标。'
+            f'三档目标分别对应不同的资源投入和激励方案,给团队明确的方向感。'
+        )
+    items.append({'title': '🎯 目标可行性分析', 'content': feasibility})
+
+    # ② 关键假设
+    assumptions = context.get('key_assumptions', [])
+    if assumptions:
+        assump_text = (
+            f'下月目标达成的关键假设:{"、".join(assumptions[:3])}。'
+            f'关键假设是目标可行性的前提条件,任一假设失效都可能导致目标无法达成。'
+            f'建议每周Review假设状态:绿色(稳定)、黄色(波动)、红色(失效),红色假设需在48h内启动应对预案。'
+        )
+    else:
+        assump_text = (
+            f'下月目标的关键假设尚未明确。建议至少列出3个最关键假设:'
+            f'(1)大客户复购率维持本月水平;'
+            f'(2)主要船期无大面积延误;'
+            f'(3)汇率波动不超过5%。'
+            f'假设清单是风险管理的起点,没有假设的目标只是愿望。'
+        )
+    items.append({'title': '🔑 关键假设', 'content': assump_text})
+
+    # ③ 风险场景(最坏情况)
+    if risks:
+        worst_gap = context.get('worst_case_gap', 0)
+        risk_text = (
+            f'已识别风险:{"、".join([r.get("title", "") for r in risks[:3]])}。'
+            f'最坏情况下,预计月度缺口约{worst_gap}台。'
+            f'缺口弥补方案:(1)加速A→B转化,释放存量pipeline;(2)启动紧急促销,刺激短期下单;'
+            f'(3)协调生产加班,压缩交付周期以提升客户信心。'
+            f'风险场景应每两周更新一次,确保预案与市场变化同步。'
+        )
+    else:
+        risk_text = (
+            f'本月风险清单为空。建议基于历史数据和当前pipeline,推演3个最坏场景:'
+            f'(1)Top 1客户延迟下单,缺口30%;(2)船期延误2周,E阶段积压导致客户暂停新签;'
+            f'(3)目的国进口政策突变,已锁定合同无法执行。每个场景设定触发条件和应对动作。'
+        )
+    items.append({'title': '⚠️ 风险场景(最坏情况)', 'content': risk_text})
+
+    # ④ Contingency Plan
+    contingency = (
+        f'Contingency Plan原则:当实际进度低于目标70%或关键假设失效时,自动触发应急预案。'
+        f'预案内容包括:'
+        f'(1)资源重新配置:将低效区域人力调至高效区域;'
+        f'(2)目标动态调整:按"保利润/保现金流/保市场份额"优先级重新排序;'
+        f'(3)外部资源调用:启动代理商紧急补单、申请总部促销资源、协调备用物流商。'
+        f'预案需在月初制定并获管理层批准,确保危机发生时24h内可执行。'
+    )
+    items.append({'title': '🛡️ Contingency Plan', 'content': contingency})
+
+    # ⑤ 里程碑与复盘机制
+    milestones = context.get('monthly_milestones', [])
+    if milestones:
+        mile_text = (
+            f'下月关键里程碑:{"、".join(milestones[:3])}。'
+            f'里程碑应满足可验证性:有明确交付物、验收人和截止时间。'
+            f'建议建立"双周复盘"机制:每月15日和月底对照里程碑,偏差大于20%时触发专项调整。'
+        )
+    else:
+        mile_text = (
+            f'下月里程碑尚未设定。建议按"第一周签约冲刺、第二周生产锁定、第三周尾款回收、第四周交付收官"'
+            f'设置4个周里程碑,每个里程碑有量化指标和责任人。没有里程碑的月度计划只是方向,不是计划。'
+        )
+    items.append({'title': '🚩 里程碑与复盘机制', 'content': mile_text})
+
+    return items
+
+
+# =============================================================================
+# HELPER FUNCTIONS
+# =============================================================================
+
+def _pct_change(curr, prev):
+    if prev and prev != 0:
+        return round((curr - prev) / prev * 100, 1)
+    return None
+
+
+def _fmt_pct(val):
+    if val is None:
+        return '—'
+    sign = '+' if val >= 0 else ''
+    return f'{sign}{val:.1f}%'
+
+
+def _fmt_chg_dir(val):
+    if val is None:
+        return ''
+    return '增加' if val >= 0 else '减少'
+
+
+def _safe_div(a, b):
+    return round(a / b, 1) if b else 0

+ 1152 - 0
generate-data-report-ppt/scripts/metrics_calculator.py

@@ -0,0 +1,1152 @@
+"""
+Metrics calculation engine for daily, weekly, and monthly reports.
+Enhanced with deep analytics: structured insights, conversion rates,
+regional top countries, per-capita performance, overdue details.
+"""
+import pandas as pd
+from datetime import datetime, timedelta
+from collections import Counter
+
+STATUS_CODES = ['A', 'B', 'C', 'D', 'E', 'F']
+STATUS_NAMES = {
+    'A': '合同拟定中',
+    'B': '已锁定合同待付订金',
+    'C': '已付订金待生产',
+    'D': '已生产待付尾款',
+    'E': '已付尾款待发运',
+    'F': '已发运',
+}
+STATUS_SHORT = {
+    'A': '合同拟定',
+    'B': '已锁定',
+    'C': '已付订金',
+    'D': '生产中',
+    'E': '待发运',
+    'F': '已发运',
+}
+
+REGIONS = {
+    '亚洲': ['中国', '泰国', '缅甸', '柬埔寨', '老挝', '越南', '菲律宾', '马来西亚',
+           '印度尼西亚', '新加坡', '斯里兰卡', '尼泊尔', '孟加拉国', '印度',
+           '巴基斯坦', '科威特', '沙特阿拉伯', '约旦', '伊拉克', '黎巴嫩',
+           '阿联酋', '卡塔尔', '阿曼', '也门', '叙利亚', '以色列', '土耳其'],
+    '非洲': ['埃及', '阿尔及利亚', '尼日利亚', '肯尼亚', '加纳', '南非', '摩洛哥',
+           '突尼斯', '利比亚', '苏丹', '埃塞俄比亚', '索马里', '乌干达', '坦桑尼亚',
+           '卢旺达', '赞比亚', '津巴布韦', '博茨瓦纳', '马达加斯加', '毛里求斯',
+           '塞内加尔', '马里', '布基纳法索', '科特迪瓦', '尼日尔', '喀麦隆',
+           '中非', '赤道几内亚', '加蓬', '刚果', '安哥拉'],
+    '拉美/加勒比': ['墨西哥', '危地马拉', '伯利兹', '洪都拉斯', '萨尔瓦多', '尼加拉瓜',
+               '哥斯达黎加', '巴拿马', '哥伦比亚', '委内瑞拉', '圭亚那', '苏里南',
+               '厄瓜多尔', '秘鲁', '玻利维亚', '巴西', '巴拉圭', '智利', '阿根廷',
+               '乌拉圭', '古巴', '牙买加', '海地', '多米尼加', '波多黎各',
+               '巴哈马', '格林纳达', '巴巴多斯', '特立尼达和多巴哥',
+               '阿鲁巴', '库拉索', '圣巴泰勒米'],
+    '中东': ['科威特', '沙特阿拉伯', '约旦', '伊拉克', '黎巴嫩', '阿联酋', '卡塔尔',
+           '阿曼', '也门', '叙利亚', '以色列', '土耳其', '伊朗', '巴林'],
+    '欧洲': ['俄罗斯', '格鲁吉亚', '意大利', '德国', '法国', '英国', '西班牙',
+           '葡萄牙', '荷兰', '比利时', '瑞士', '奥地利', '波兰', '乌克兰',
+           '白俄罗斯', '罗马尼亚', '保加利亚', '希腊', '塞尔维亚', '匈牙利',
+           '捷克', '斯洛伐克', '瑞典', '挪威', '丹麦', '芬兰', '冰岛',
+           '爱尔兰', '中国澳门'],
+}
+
+
+def _get_region(country: str) -> str:
+    for region, countries in REGIONS.items():
+        if country in countries:
+            return region
+    return '其他'
+
+
+def _pct_change(curr, prev):
+    """Calculate percentage change. Returns None if previous base is 0 or None."""
+    if prev is None or prev == 0:
+        return None
+    return round((curr - prev) / prev * 100, 1)
+
+
+def _safe_div(a, b):
+    return round(a / b, 1) if b else 0
+
+
+# ==============================================================================
+# DAILY METRICS
+# ==============================================================================
+
+def calc_daily_metrics(df: pd.DataFrame, prev_df: pd.DataFrame = None,
+                       same_day_last_week_df: pd.DataFrame = None) -> dict:
+    metrics = {}
+
+    # Core counts
+    metrics['tracking_orders'] = len(df)
+    metrics['total_qty'] = int(df['order_qty'].sum()) if 'order_qty' in df.columns else 0
+    metrics['updated_orders'] = int(df['is_updated_flag'].sum()) if 'is_updated_flag' in df.columns else 0
+    metrics['shipped_orders'] = int((df['status_code'] == 'F').sum()) if 'status_code' in df.columns else 0
+    metrics['support_requests'] = int(df['support_request'].notna().sum()) if 'support_request' in df.columns else 0
+    metrics['forecast_next'] = int(df['forecast_may'].sum()) if 'forecast_may' in df.columns else 0
+
+    # Average order size (单均台数)
+    metrics['avg_order_size'] = _safe_div(metrics['total_qty'], metrics['tracking_orders'])
+
+    # Daily average orders (日均订单数) — for daily it's 1 day, but keep consistent
+    days = df['_data_date'].nunique() if '_data_date' in df.columns else 1
+    metrics['avg_daily_orders'] = _safe_div(metrics['tracking_orders'], days)
+
+    # Comparisons
+    if prev_df is not None and len(prev_df) > 0:
+        metrics['prev_tracking_orders'] = len(prev_df)
+        metrics['prev_total_qty'] = int(prev_df['order_qty'].sum()) if 'order_qty' in prev_df.columns else 0
+        metrics['prev_updated_orders'] = int(prev_df['is_updated_flag'].sum()) if 'is_updated_flag' in prev_df.columns else 0
+        metrics['prev_shipped_orders'] = int((prev_df['status_code'] == 'F').sum()) if 'status_code' in prev_df.columns else 0
+        metrics['prev_support_requests'] = int(prev_df['support_request'].notna().sum()) if 'support_request' in prev_df.columns else 0
+        metrics['prev_forecast_next'] = int(prev_df['forecast_may'].sum()) if 'forecast_may' in prev_df.columns else 0
+        metrics['prev_avg_order_size'] = _safe_div(metrics['prev_total_qty'], metrics['prev_tracking_orders'])
+    else:
+        metrics['prev_tracking_orders'] = 0
+        metrics['prev_total_qty'] = 0
+        metrics['prev_avg_order_size'] = 0
+
+    # Status distribution
+    if 'status_code' in df.columns:
+        status_counts = df['status_code'].value_counts().to_dict()
+        metrics['status_dist'] = {STATUS_NAMES.get(k, k): int(status_counts.get(k, 0)) for k in STATUS_CODES}
+        # Find max status
+        max_status = max(metrics['status_dist'].items(), key=lambda x: x[1])
+        metrics['status_max'] = {'name': max_status[0], 'count': max_status[1]}
+        # Production-related share (C+D)
+        prod = int(status_counts.get('C', 0)) + int(status_counts.get('D', 0))
+        metrics['production_share'] = round(prod / len(df) * 100, 1) if len(df) > 0 else 0
+    else:
+        metrics['status_dist'] = {}
+        metrics['status_max'] = {'name': '', 'count': 0}
+        metrics['production_share'] = 0
+
+    # Status WoW (vs previous day) — for daily report page 4
+    metrics['status_wow'] = {}
+    if 'status_code' in df.columns and prev_df is not None and 'status_code' in prev_df.columns:
+        for code in STATUS_CODES:
+            curr = int((df['status_code'] == code).sum())
+            prev = int((prev_df['status_code'] == code).sum())
+            metrics['status_wow'][STATUS_NAMES[code]] = {
+                'current': curr,
+                'previous': prev,
+                'change_pct': _pct_change(curr, prev)
+            }
+
+    # Owner distribution
+    if 'owner' in df.columns:
+        owner_counts = df['owner'].value_counts().head(10).to_dict()
+        metrics['owner_dist'] = dict(sorted(owner_counts.items(), key=lambda x: -x[1]))
+    else:
+        metrics['owner_dist'] = {}
+
+    # Country TOP8
+    if 'country' in df.columns:
+        country_counts = df.groupby('country')['order_qty'].sum().sort_values(ascending=False).head(8).to_dict()
+        metrics['country_top8'] = country_counts
+    else:
+        metrics['country_top8'] = {}
+
+    # Overdue orders detail (status A > 30 days) — include contract_no
+    metrics['overdue_orders'] = []
+    if 'status_code' in df.columns and 'tracking_days' in df.columns and 'country' in df.columns:
+        overdue = df[(df['status_code'] == 'A') & (df['tracking_days'] > 30)].copy()
+        if len(overdue) > 0:
+            overdue = overdue.sort_values('tracking_days', ascending=False)
+            metrics['overdue_orders'] = [
+                {
+                    'contract_no': str(row['contract_no']) if pd.notna(row.get('contract_no')) else '',
+                    'country': row['country'],
+                    'days': int(row['tracking_days'])
+                }
+                for _, row in overdue.head(10).iterrows()
+            ]
+
+    # Alerts
+    metrics['alerts'] = _extract_alerts(df)
+
+    # Support request categories
+    metrics['support_categories'] = _categorize_support(df)
+    if metrics['support_categories']:
+        top_cat = max(metrics['support_categories'].items(), key=lambda x: x[1])
+        metrics['support_top_category'] = {'name': top_cat[0], 'count': top_cat[1]}
+    else:
+        metrics['support_top_category'] = {'name': '', 'count': 0}
+
+    return metrics
+
+
+# ==============================================================================
+# WEEKLY METRICS
+# ==============================================================================
+
+def calc_weekly_metrics(df: pd.DataFrame, prev_df: pd.DataFrame = None) -> dict:
+    metrics = {}
+
+    # Core KPIs
+    metrics['tracking_orders'] = len(df)
+    metrics['total_qty'] = int(df['order_qty'].sum()) if 'order_qty' in df.columns else 0
+    metrics['shipped_orders'] = int((df['status_code'] == 'F').sum()) if 'status_code' in df.columns else 0
+    metrics['countries'] = df['country'].nunique() if 'country' in df.columns else 0
+    metrics['updated_orders'] = int(df['is_updated_flag'].sum()) if 'is_updated_flag' in df.columns else 0
+    metrics['forecast_next'] = int(df['forecast_may'].sum()) if 'forecast_may' in df.columns else 0
+
+    # Daily averages
+    days = df['_data_date'].nunique() if '_data_date' in df.columns else 7
+    metrics['avg_daily_orders'] = _safe_div(metrics['tracking_orders'], days)
+    metrics['avg_daily_qty'] = _safe_div(metrics['total_qty'], days)
+
+    # Average quantity per order (单均台数)
+    metrics['avg_qty_per_order'] = _safe_div(metrics['total_qty'], metrics['tracking_orders'])
+
+    # WoW comparisons
+    if prev_df is not None and len(prev_df) > 0:
+        metrics['prev_tracking_orders'] = len(prev_df)
+        metrics['prev_total_qty'] = int(prev_df['order_qty'].sum()) if 'order_qty' in prev_df.columns else 0
+        metrics['prev_shipped_orders'] = int((prev_df['status_code'] == 'F').sum()) if 'status_code' in prev_df.columns else 0
+        metrics['prev_updated_orders'] = int(prev_df['is_updated_flag'].sum()) if 'is_updated_flag' in prev_df.columns else 0
+        metrics['prev_forecast_next'] = int(prev_df['forecast_may'].sum()) if 'forecast_may' in prev_df.columns else 0
+        prev_days = prev_df['_data_date'].nunique() if '_data_date' in prev_df.columns else days
+        metrics['prev_avg_daily_orders'] = _safe_div(metrics['prev_tracking_orders'], prev_days)
+    else:
+        metrics['prev_tracking_orders'] = 0
+
+    # Status stage WoW
+    metrics['status_wow'] = {}
+    if 'status_code' in df.columns and prev_df is not None and 'status_code' in prev_df.columns:
+        for code in STATUS_CODES:
+            curr = int((df['status_code'] == code).sum())
+            prev = int((prev_df['status_code'] == code).sum())
+            metrics['status_wow'][STATUS_NAMES[code]] = {
+                'current': curr,
+                'previous': prev,
+                'change_pct': _pct_change(curr, prev)
+            }
+
+    # 7-day trend
+    if '_data_date' in df.columns:
+        trend = df.groupby('_data_date').size().sort_index()
+        metrics['daily_trend'] = {k.strftime('%m/%d'): int(v) for k, v in trend.items()}
+        # Days above previous week average
+        if metrics.get('prev_avg_daily_orders', 0) > 0:
+            metrics['days_above_prev_avg'] = sum(1 for v in metrics['daily_trend'].values() if v > metrics['prev_avg_daily_orders'])
+        else:
+            metrics['days_above_prev_avg'] = len(metrics['daily_trend'])
+    else:
+        metrics['daily_trend'] = {}
+        metrics['days_above_prev_avg'] = 0
+
+    # Region distribution with top countries per region
+    if 'country' in df.columns:
+        df['region'] = df['country'].apply(_get_region)
+        region_qty = df.groupby('region')['order_qty'].sum().sort_values(ascending=False).to_dict()
+        total = sum(region_qty.values())
+        metrics['region_dist'] = {}
+        for region, qty in region_qty.items():
+            region_df = df[df['region'] == region]
+            top3 = region_df.groupby('country')['order_qty'].sum().sort_values(ascending=False).head(3)
+            metrics['region_dist'][region] = {
+                'qty': int(qty),
+                'pct': round(qty / total * 100, 1) if total else 0,
+                'top_countries': [{'country': c, 'qty': int(v)} for c, v in top3.to_dict().items()]
+            }
+    else:
+        metrics['region_dist'] = {}
+
+    # Top countries with WoW change
+    if 'country' in df.columns:
+        top_countries = df.groupby('country')['order_qty'].sum().sort_values(ascending=False).head(15).to_dict()
+        metrics['top_countries'] = {k: int(v) for k, v in top_countries.items()}
+        # Calculate change vs previous week
+        metrics['top_countries_change'] = {}
+        if prev_df is not None and 'country' in prev_df.columns:
+            prev_top = prev_df.groupby('country')['order_qty'].sum().to_dict()
+            for country, qty in metrics['top_countries'].items():
+                prev_qty = prev_top.get(country, 0)
+                metrics['top_countries_change'][country] = {
+                    'qty': qty,
+                    'prev_qty': int(prev_qty),
+                    'change_pct': _pct_change(qty, prev_qty)
+                }
+    else:
+        metrics['top_countries'] = {}
+        metrics['top_countries_change'] = {}
+
+    # TOP6 country concentration
+    if 'country' in df.columns and metrics['total_qty'] > 0:
+        top6_qty = df.groupby('country')['order_qty'].sum().sort_values(ascending=False).head(6).sum()
+        metrics['top6_concentration_pct'] = round(top6_qty / metrics['total_qty'] * 100, 1)
+    else:
+        metrics['top6_concentration_pct'] = 0.0
+
+    # Team performance
+    if 'owner' in df.columns:
+        team = df.groupby('owner').agg(
+            orders=('contract_no', 'count'),
+            qty=('order_qty', 'sum')
+        ).sort_values('orders', ascending=False)
+        metrics['team'] = {
+            'owners': team['orders'].to_dict(),
+            'qty': team['qty'].to_dict(),
+        }
+        metrics['per_capita_orders'] = _safe_div(metrics['tracking_orders'], len(team))
+        if prev_df is not None and 'owner' in prev_df.columns:
+            prev_team = prev_df.groupby('owner').size().to_dict()
+            metrics['team_wow'] = {
+                owner: {'current': int(v), 'previous': int(prev_team.get(owner, 0)),
+                        'change': int(v - prev_team.get(owner, 0))}
+                for owner, v in team['orders'].items()
+            }
+    else:
+        metrics['team'] = {'owners': {}, 'qty': {}}
+        metrics['per_capita_orders'] = 0
+        metrics['team_wow'] = {}
+
+    # Support request categories
+    metrics['support_categories'] = _categorize_support(df)
+
+    # Issues & next week plan (auto-generated based on current data)
+    metrics['issues'] = _extract_weekly_issues(df)
+
+    # Calculate stage counts for goal generation
+    status_counts = {}
+    if 'status_code' in df.columns:
+        status_counts = {code: int((df['status_code'] == code).sum()) for code in STATUS_CODES}
+
+    pending_shipment = status_counts.get('E', 0)
+    pending_payment = status_counts.get('D', 0)
+    status_a = status_counts.get('A', 0)
+    new_sign = metrics['updated_orders']
+
+    metrics['next_week_goals'] = [
+        {
+            'id': 'G1',
+            'title': '交付冲刺:推动待发运清零',
+            'detail': f'确保{pending_shipment}单待发运订单完成发运交付',
+            'number': pending_shipment,
+        },
+        {
+            'id': 'G2',
+            'title': '尾款回收:加速资金回笼',
+            'detail': f'推进{pending_payment}单完成尾款支付转化',
+            'number': pending_payment,
+        },
+        {
+            'id': 'G3',
+            'title': '新签拓展:维持增长势头',
+            'detail': f'新签合同目标{max(new_sign, 5)}单,推动业务增长',
+            'number': max(new_sign, 5),
+        },
+        {
+            'id': 'G4',
+            'title': '转化提速:A→B阶段突破',
+            'detail': f'推动{status_a}单合同拟定进入锁定阶段',
+            'number': status_a,
+        },
+    ]
+
+    return metrics
+
+
+# ==============================================================================
+# MONTHLY METRICS
+# ==============================================================================
+
+def calc_monthly_metrics(df: pd.DataFrame, prev_df: pd.DataFrame = None,
+                         yoy_df: pd.DataFrame = None) -> dict:
+    metrics = {}
+
+    # Core KPIs
+    metrics['total_contracts'] = len(df)
+    metrics['total_qty'] = int(df['order_qty'].sum()) if 'order_qty' in df.columns else 0
+    metrics['new_contracts'] = int(df['is_updated_flag'].sum()) if 'is_updated_flag' in df.columns else 0
+    metrics['new_qty'] = int(df[df['is_updated_flag'] == True]['order_qty'].sum()) if 'order_qty' in df.columns else 0
+    metrics['shipped_orders'] = int((df['status_code'] == 'F').sum()) if 'status_code' in df.columns else 0
+    metrics['shipped_qty'] = int(df[df['status_code'] == 'F']['order_qty'].sum()) if 'order_qty' in df.columns else 0
+    metrics['countries'] = df['country'].nunique() if 'country' in df.columns else 0
+    metrics['support_count'] = int(df['support_request'].notna().sum()) if 'support_request' in df.columns else 0
+    metrics['support_pct'] = round(metrics['support_count'] / len(df) * 100, 1) if len(df) > 0 else 0
+    metrics['forecast_next'] = int(df['forecast_may'].sum()) if 'forecast_may' in df.columns else 0
+
+    # Daily average
+    days = df['_data_date'].nunique() if '_data_date' in df.columns else 30
+    metrics['avg_daily_orders'] = _safe_div(metrics['total_contracts'], days)
+
+    # Average quantity per order (单均台数)
+    metrics['avg_qty_per_order'] = _safe_div(metrics['total_qty'], metrics['total_contracts'])
+
+    # Comparisons
+    if prev_df is not None and len(prev_df) > 0:
+        metrics['prev_total_contracts'] = len(prev_df)
+        metrics['prev_total_qty'] = int(prev_df['order_qty'].sum()) if 'order_qty' in prev_df.columns else 0
+    if yoy_df is not None and len(yoy_df) > 0:
+        metrics['yoy_total_contracts'] = len(yoy_df)
+        metrics['yoy_total_qty'] = int(yoy_df['order_qty'].sum()) if 'order_qty' in yoy_df.columns else 0
+
+    # Status funnel with stage analysis and percentages
+    total_orders = len(df)
+    if 'status_code' in df.columns:
+        funnel = {}
+        for code in STATUS_CODES:
+            count = int((df['status_code'] == code).sum())
+            qty = int(df[df['status_code'] == code]['order_qty'].sum()) if 'order_qty' in df.columns else 0
+            funnel[STATUS_NAMES[code]] = {
+                'orders': count,
+                'qty': qty,
+                'pct': round(count / total_orders * 100, 1) if total_orders else 0,
+            }
+        metrics['status_funnel'] = funnel
+
+        # Stage analysis: early (A+B), mid (C+D), late (E+F)
+        early = int((df['status_code'].isin(['A', 'B'])).sum())
+        mid = int((df['status_code'].isin(['C', 'D'])).sum())
+        late = int((df['status_code'].isin(['E', 'F'])).sum())
+        metrics['stage_analysis'] = {
+            'early': {'orders': early, 'pct': round(early / total_orders * 100, 1) if total_orders else 0},
+            'mid': {'orders': mid, 'pct': round(mid / total_orders * 100, 1) if total_orders else 0},
+            'late': {'orders': late, 'pct': round(late / total_orders * 100, 1) if total_orders else 0},
+        }
+        # Pending stages
+        metrics['pending_shipment'] = {'orders': funnel.get('已付尾款待发运', {}).get('orders', 0),
+                                       'qty': funnel.get('已付尾款待发运', {}).get('qty', 0)}
+        metrics['pending_payment'] = {'orders': funnel.get('已生产待付尾款', {}).get('orders', 0),
+                                      'qty': funnel.get('已生产待付尾款', {}).get('qty', 0)}
+    else:
+        metrics['status_funnel'] = {}
+        metrics['stage_analysis'] = {'early': {'orders': 0, 'pct': 0}, 'mid': {'orders': 0, 'pct': 0}, 'late': {'orders': 0, 'pct': 0}}
+        metrics['pending_shipment'] = {'orders': 0, 'qty': 0}
+        metrics['pending_payment'] = {'orders': 0, 'qty': 0}
+
+    # Region distribution
+    if 'country' in df.columns:
+        df['region'] = df['country'].apply(_get_region)
+        region_data = df.groupby('region').agg(
+            orders=('contract_no', 'count'),
+            qty=('order_qty', 'sum')
+        )
+        total_qty = region_data['qty'].sum()
+        metrics['region_dist'] = {}
+        for idx, row in region_data.iterrows():
+            region_df = df[df['region'] == idx]
+            top3 = region_df.groupby('country')['order_qty'].sum().sort_values(ascending=False).head(3)
+            metrics['region_dist'][idx] = {
+                'orders': int(row['orders']), 'qty': int(row['qty']),
+                'pct': round(row['qty'] / total_qty * 100, 1) if total_qty else 0,
+                'top_countries': [{'country': c, 'qty': int(v)} for c, v in top3.to_dict().items()]
+            }
+    else:
+        metrics['region_dist'] = {}
+
+    # Top 10 countries
+    if 'country' in df.columns:
+        top10 = df.groupby('country').agg(
+            orders=('contract_no', 'count'),
+            qty=('order_qty', 'sum')
+        ).sort_values('qty', ascending=False).head(10)
+        metrics['top_countries'] = {
+            idx: {'orders': int(row['orders']), 'qty': int(row['qty'])}
+            for idx, row in top10.iterrows()
+        }
+        # MoM change
+        metrics['top_countries_change'] = {}
+        if prev_df is not None and 'country' in prev_df.columns:
+            prev_top = prev_df.groupby('country')['order_qty'].sum().to_dict()
+            for country, data in metrics['top_countries'].items():
+                prev_qty = prev_top.get(country, 0)
+                metrics['top_countries_change'][country] = {
+                    'qty': data['qty'],
+                    'prev_qty': int(prev_qty),
+                    'change_pct': _pct_change(data['qty'], prev_qty)
+                }
+    else:
+        metrics['top_countries'] = {}
+        metrics['top_countries_change'] = {}
+
+    # 30-day trend
+    if '_data_date' in df.columns:
+        trend = df.groupby('_data_date').size().sort_index()
+        metrics['daily_trend'] = {k.strftime('%m/%d'): int(v) for k, v in trend.items()}
+        dates = list(trend.index)
+        if len(dates) >= 3:
+            n = len(dates) // 3
+            early = trend.iloc[:n].mean()
+            mid = trend.iloc[n:2*n].mean()
+            late = trend.iloc[2*n:].mean()
+            metrics['trend_by_period'] = {
+                'early': round(early, 1),
+                'mid': round(mid, 1),
+                'late': round(late, 1),
+                'late_change_pct': _pct_change(late, mid)
+            }
+            metrics['peak_dates'] = trend.nlargest(2).index.strftime('%m/%d').tolist()
+    else:
+        metrics['daily_trend'] = {}
+        metrics['trend_by_period'] = {}
+        metrics['peak_dates'] = []
+
+    # Team performance
+    if 'owner' in df.columns:
+        team = df.groupby('owner').agg(
+            orders=('contract_no', 'count'),
+            qty=('order_qty', 'sum')
+        ).sort_values('orders', ascending=False)
+        metrics['team'] = {
+            idx: {'orders': int(row['orders']), 'qty': int(row['qty'])}
+            for idx, row in team.iterrows()
+        }
+        metrics['per_capita_orders'] = _safe_div(metrics['total_contracts'], len(team))
+        metrics['per_capita_qty'] = _safe_div(metrics['total_qty'], len(team))
+    else:
+        metrics['team'] = {}
+        metrics['per_capita_orders'] = 0
+        metrics['per_capita_qty'] = 0
+
+    # Support request analysis
+    metrics['support_categories'] = _categorize_support(df)
+
+    # Overdue orders detail (status A > 30 days) — include contract_no
+    metrics['overdue_orders'] = []
+    if 'status_code' in df.columns and 'tracking_days' in df.columns and 'country' in df.columns:
+        overdue = df[(df['status_code'] == 'A') & (df['tracking_days'] > 30)].copy()
+        if len(overdue) > 0:
+            overdue = overdue.sort_values('tracking_days', ascending=False)
+            metrics['overdue_orders'] = [
+                {
+                    'contract_no': str(row['contract_no']) if pd.notna(row.get('contract_no')) else '',
+                    'country': row['country'],
+                    'days': int(row['tracking_days'])
+                }
+                for _, row in overdue.head(10).iterrows()
+            ]
+
+    # Next month plan (auto-generated with real numbers)
+    pending_ship_orders = metrics.get('pending_shipment', {}).get('orders', 0)
+    pending_ship_qty = metrics.get('pending_shipment', {}).get('qty', 0)
+    pending_pay_orders = metrics.get('pending_payment', {}).get('orders', 0)
+    new_sign_target = max(metrics['new_contracts'] * 2, 10)
+    new_sign_qty_target = max(metrics['new_qty'] * 2, 100)
+    status_a_count = metrics.get('status_funnel', {}).get('合同拟定中', {}).get('orders', 0)
+
+    metrics['next_month_goals'] = [
+        {
+            'title': '目标一:交付冲刺',
+            'detail': f'预测交付{metrics.get("forecast_next", 0)}台,推动{pending_ship_orders}单待发运订单尽快发运',
+            'number': pending_ship_qty,
+        },
+        {
+            'title': '目标二:尾款回收',
+            'detail': f'推进{pending_pay_orders}单完成尾款支付,转化至发运阶段',
+            'number': pending_pay_orders,
+        },
+        {
+            'title': '目标三:新签拓展',
+            'detail': f'新签合同目标{new_sign_target}单,覆盖{new_sign_qty_target:,}台车辆',
+            'number': new_sign_target,
+        },
+        {
+            'title': '目标四:转化提速',
+            'detail': f'将{status_a_count}单A阶段合同推进至B阶段,转化率目标30%',
+            'number': status_a_count,
+        },
+        {
+            'title': '目标五:流程优化',
+            'detail': '建立跨部门支持需求SLA机制,处理时效缩短至48h',
+            'number': 0,
+        },
+    ]
+    metrics['risks'] = [
+        {'title': '流程阻塞', 'detail': '高比例订单存在支持需求,跨部门协调效率直接影响交付', 'action': '建立专项协调小组,每周跟踪'},
+        {'title': '早期订单堆积', 'detail': '大量订单停留在合同拟定阶段,转化周期过长', 'action': '优化合同审批流程,设置阶段时限'},
+        {'title': '交付缺口', 'detail': '预测交付量大,需确保生产和物流按时到位', 'action': '提前锁定生产排期和物流舱位'},
+    ]
+
+    return metrics
+
+
+# ==============================================================================
+# HELPERS
+# ==============================================================================
+
+def _categorize_support(df: pd.DataFrame) -> dict:
+    """Categorize support requests by keyword."""
+    if 'support_request' not in df.columns:
+        return {}
+    sr = df[df['support_request'].notna()]['support_request'].astype(str)
+    categories = Counter()
+    for text in sr:
+        if '财务' in text or '收款' in text or '账户' in text or '汇率' in text:
+            categories['财务确认'] += 1
+        elif '售后' in text or '配件' in text:
+            categories['售后/配件'] += 1
+        elif '法务' in text or '合同' in text or '条款' in text:
+            categories['法务审核'] += 1
+        elif '质量' in text or '检测' in text:
+            categories['质量检测'] += 1
+        elif '物流' in text or '船期' in text or '运输' in text:
+            categories['物流/船期'] += 1
+        elif 'IT' in text.upper() or '系统' in text or 'OOMS' in text:
+            categories['IT/系统'] += 1
+        else:
+            categories['其他'] += 1
+    return dict(categories)
+
+
+def _extract_alerts(df: pd.DataFrame) -> list:
+    alerts = []
+
+    # 1. Overdue contracts (>30 days in status A)
+    if 'status_code' in df.columns and 'tracking_days' in df.columns:
+        overdue = df[(df['status_code'] == 'A') & (df['tracking_days'] > 30)]
+        if len(overdue) > 0:
+            max_days = int(overdue['tracking_days'].max())
+            countries = overdue['country'].dropna().unique().tolist()[:3] if 'country' in df.columns else []
+            alerts.append({
+                'level': '严重',
+                'title': f'{len(overdue)}单合同拟定中超30天',
+                'detail': f'最长{max_days}天,涉及{"、".join(countries)}等,需尽快推动签订'
+            })
+
+    # 2. System errors (OOMS, etc.)
+    if 'support_request' in df.columns:
+        system_issues = df[df['support_request'].astype(str).str.contains('OOMS|系统|无匹配', na=False)]
+        if len(system_issues) > 0:
+            alerts.append({
+                'level': '关注',
+                'title': f'{len(system_issues)}项系统异常',
+                'detail': '国家数据无匹配或OOMS录入问题,已联系IT处理'
+            })
+
+    # 3. Pending support requests
+    if 'support_request' in df.columns:
+        pending = df['support_request'].notna().sum()
+        if pending > 0:
+            alerts.append({
+                'level': '警告',
+                'title': f'{pending}项支持需求待处理',
+                'detail': '需跨部门协调推进'
+            })
+
+    return alerts
+
+
+def _extract_weekly_issues(df: pd.DataFrame) -> list:
+    issues = []
+    if 'support_request' in df.columns:
+        system = df[df['support_request'].astype(str).str.contains('OOMS|系统|无匹配', na=False)]
+        if len(system) > 0:
+            issues.append({
+                'severity': '严重',
+                'title': 'OOMS系统数据匹配问题',
+                'detail': f'{len(system)}笔订单因国家字典缺失无法录入,影响发运流程',
+                'action': '联系IT部门批量补充国家字典;短期建立手动录入通道'
+            })
+    if 'status_code' in df.columns and 'tracking_days' in df.columns:
+        delay = df[(df['status_code'].isin(['D', 'E'])) & (df['tracking_days'] > 45)]
+        if len(delay) > 0:
+            issues.append({
+                'severity': '中度',
+                'title': '客户付款延迟',
+                'detail': f'{len(delay)}笔订单等待定金/尾款支付,影响生产排期',
+                'action': '建立客户付款预警机制,提前3天提醒到期付款'
+            })
+        # Import license delays
+        license_delay = df[(df['status_code'].isin(['C', 'D'])) & (df['tracking_days'] > 60)]
+        if len(license_delay) > 0:
+            issues.append({
+                'severity': '中度',
+                'title': '部分国家进口许可证/认证待办',
+                'detail': f'约{len(license_delay)}笔订单受影响,目的国准入认证办理延迟',
+                'action': '提前启动目的国准入认证流程,建立目的港代理协作网络'
+            })
+    return issues
+
+
+# ==============================================================================
+# DEEP INSIGHTS ENGINE
+# ==============================================================================
+
+def generate_deep_insights(report_type: str, page_type: str, metrics: dict, **context) -> list[dict]:
+    """
+    Generate structured deep-analysis insight items.
+    Each item: {'title': str, 'content': str}
+    Target: 5-6 items per page, each content >= 40-60 Chinese characters.
+    """
+    if report_type == 'daily':
+        return _daily_insights(page_type, metrics, context)
+    if report_type == 'weekly':
+        return _weekly_insights(page_type, metrics, context)
+    if report_type == 'monthly':
+        return _monthly_insights(page_type, metrics, context)
+    return []
+
+
+def _daily_insights(page_type: str, metrics: dict, context: dict) -> list[dict]:
+    if page_type == 'trend':
+        return _insight_daily_trend(metrics, context)
+    if page_type == 'status':
+        return _insight_daily_status(metrics, context)
+    if page_type == 'owner':
+        return _insight_daily_owner(metrics, context)
+    if page_type == 'country':
+        return _insight_daily_country(metrics, context)
+    if page_type == 'alert':
+        return _insight_daily_alert(metrics, context)
+    if page_type == 'action':
+        return _insight_daily_action(metrics, context)
+    return []
+
+
+# ------------------------------------------------------------------------------
+# DAILY PAGE 3 — 近10天订单趋势
+# ------------------------------------------------------------------------------
+
+def _insight_daily_trend(metrics: dict, context: dict) -> list[dict]:
+    items = []
+    trend_dates = context.get('trend_dates', [])
+    trend_vals = context.get('trend_vals', [])
+    prev_metrics = context.get('prev_metrics', {})
+
+    curr_orders = metrics.get('tracking_orders', 0)
+    prev_orders = prev_metrics.get('tracking_orders', 0) if prev_metrics else 0
+    curr_qty = metrics.get('total_qty', 0)
+    prev_qty = prev_metrics.get('total_qty', 0) if prev_metrics else 0
+    avg_size = metrics.get('avg_order_size', 0)
+    prev_avg_size = prev_metrics.get('avg_order_size', 0) if prev_metrics else 0
+    updated = metrics.get('updated_orders', 0)
+    prev_updated = prev_metrics.get('updated_orders', 0) if prev_metrics else 0
+
+    # ① 订单规模分析(现状)
+    scale_text = f'今日订单量{curr_orders}单'
+    if prev_orders > 0:
+        diff = curr_orders - prev_orders
+        pct = _pct_change(curr_orders, prev_orders)
+        scale_text += f',较昨日{"增加" if diff >= 0 else "减少"}{abs(diff)}单({_fmt_pct(pct)})'
+    if curr_qty > 0:
+        scale_text += f',订单总数量{curr_qty}台'
+        if prev_qty > 0:
+            qdiff = curr_qty - prev_qty
+            qpct = _pct_change(curr_qty, prev_qty)
+            scale_text += f',较昨日{"增加" if qdiff >= 0 else "减少"}{abs(qdiff)}台({_fmt_pct(qpct)})'
+    if avg_size > 0:
+        scale_text += f'。单笔订单平均规模{avg_size:.0f}台'
+        if prev_avg_size > 0:
+            adiff = avg_size - prev_avg_size
+            scale_text += f',较昨日{"上升" if adiff >= 0 else "下降"}{abs(adiff):.0f}台,{"说明大客户下单节奏恢复" if adiff > 0 else "说明中小客户占比提升"}'
+        else:
+            scale_text += ',大客户下单节奏有所恢复'
+    scale_text += '。'
+    items.append({'title': '💡 订单规模分析', 'content': scale_text})
+
+    # ② 趋势归因(Why)
+    if len(trend_vals) >= 3:
+        recent = trend_vals[-3:]
+        if recent[-1] > recent[-2] and recent[-2] < recent[0]:
+            attr_text = f'连续{len([v for v in trend_vals[-3:] if v < trend_vals[-1]])}日回落后今日反弹至{curr_orders}单,主要受大客户追加订单驱动。剔除异常大单后,日均订单仍维持在{sum(trend_vals)//len(trend_vals)}单左右,基础盘稳定。建议关注反弹是否具备持续性,明日若维持该水平可确认回升趋势。'
+        elif recent[-1] < recent[-2]:
+            attr_text = f'近期呈回落趋势,今日{curr_orders}单较峰值下降。需排查是否为节假日效应或客户付款周期影响,建议主动触达高意向客户,避免订单持续流失。'
+        else:
+            attr_text = f'近3日订单量呈{"上升" if recent[-1] >= recent[0] else "震荡"}态势,今日{curr_orders}单{"为阶段高点" if recent[-1] == max(recent) else "处于正常区间"}。建议结合pipeline评估后续走势。'
+    else:
+        attr_text = f'今日订单量{curr_orders}单,较前期{"回升" if prev_orders and curr_orders > prev_orders else "平稳"}。建议持续跟踪大客户下单节奏,确保基础盘稳定。'
+    items.append({'title': '📈 趋势归因', 'content': attr_text})
+
+    # ③ 异常波动识别
+    if len(trend_vals) >= 2 and trend_dates:
+        peak = max(trend_vals)
+        peak_idx = trend_vals.index(peak)
+        low = min(trend_vals)
+        low_idx = trend_vals.index(low)
+        peak_date = trend_dates[peak_idx]
+        low_date = trend_dates[low_idx]
+        amplitude = _pct_change(peak, low)
+        amp_str = f'{abs(amplitude):.1f}%' if amplitude is not None else '显著'
+        vola_text = f'{peak_date}峰值{peak}单为{"近10天" if len(trend_vals) >= 10 else "近期"}最高,{low_date}低谷{low}单为最低,波动幅度达{amp_str}。建议排查低谷日是否有系统录入延迟或客户付款周期影响,识别外部干扰因素。'
+    else:
+        vola_text = '近期订单数据样本不足,暂无法识别异常波动模式。建议积累更多数据后再做波动性分析。'
+    items.append({'title': '⚠️ 异常波动识别', 'content': vola_text})
+
+    # ④ 活跃度关联分析
+    if updated > 0 or prev_updated > 0:
+        act_text = f'今日进度更新{updated}单'
+        if prev_updated > 0:
+            udiff = updated - prev_updated
+            upct = _pct_change(updated, prev_updated)
+            act_text += f',较昨日{"增加" if udiff >= 0 else "减少"}{abs(udiff)}单({_fmt_pct(upct)})'
+        act_text += ',活跃度与订单量呈正相关。但需关注更新质量:建议统计更新订单中进入下一阶段的比例,若转化率偏低,则需聚焦推动而非单纯记录更新。'
+    else:
+        act_text = '今日暂无进度更新数据。建议建立每日更新跟进机制,确保在跟订单有实质性推进。'
+    items.append({'title': '📊 活跃度关联分析', 'content': act_text})
+
+    # ⑤ 短期预测
+    if len(trend_vals) >= 3:
+        avg_recent = sum(trend_vals[-3:]) / 3
+        pred_low = int(avg_recent * 0.9)
+        pred_high = int(avg_recent * 1.15)
+        pred_text = f'基于当前pipeline和近3日均值{avg_recent:.0f}单,预计未来3天日均订单维持在{pred_low}-{pred_high}单区间。若大客户持续下单,下半周有望突破{pred_high}单;若客户侧审批延迟,则可能回落至{pred_low}单以下。建议每日早会review pipeline健康度。'
+    else:
+        pred_text = f'当前在跟订单{curr_orders}单,基础pipeline支撑下,预计明日订单量维持在{max(1, int(curr_orders * 0.85))}-{int(curr_orders * 1.1)}单区间。建议重点关注A阶段合同推进速度。'
+    items.append({'title': '🔮 短期预测', 'content': pred_text})
+
+    return items
+
+
+# ------------------------------------------------------------------------------
+# DAILY PAGE 4 — 订单状态分布
+# ------------------------------------------------------------------------------
+
+def _insight_daily_status(metrics: dict, context: dict) -> list[dict]:
+    items = []
+    status_dist = metrics.get('status_dist', {})
+    prev_status_dist = context.get('prev_status_dist', {})
+    total = sum(status_dist.values()) if status_dist else 0
+    if not total:
+        return [{'title': '💡 结构诊断', 'content': '暂无订单状态数据,无法进行分析。建议检查数据源完整性。'}]
+
+    a = status_dist.get('合同拟定中', 0)
+    b = status_dist.get('已锁定合同待付订金', 0)
+    c = status_dist.get('已付订金待生产', 0)
+    d = status_dist.get('已生产待付尾款', 0)
+    e = status_dist.get('已付尾款待发运', 0)
+    f = status_dist.get('已发运', 0)
+    prev_a = prev_status_dist.get('合同拟定中', 0) if prev_status_dist else 0
+    prev_c = prev_status_dist.get('已付订金待生产', 0) if prev_status_dist else 0
+    prev_d = prev_status_dist.get('已生产待付尾款', 0) if prev_status_dist else 0
+    prev_e = prev_status_dist.get('已付尾款待发运', 0) if prev_status_dist else 0
+
+    # ① 结构诊断(现状)
+    max_name = max(status_dist.items(), key=lambda x: x[1])[0]
+    max_val = status_dist[max_name]
+    prod_cd = c + d
+    early_ab = a + b
+    struct_text = f'{max_name}占比最高({max_val}单,{max_val/total*100:.1f}%)'
+    if prod_cd > 0:
+        struct_text += f'。生产端(C+D)合计{prod_cd}单({prod_cd/total*100:.1f}%),生产推进力度{"加大" if prev_c + prev_d < prod_cd else "维持"}'
+    if early_ab > 0:
+        struct_text += f'。前期pipeline(A+B)仍有{early_ab}单({early_ab/total*100:.1f}%),合同转化是当前瓶颈'
+    struct_text += '。整体结构显示订单正从前期向中后期推进,但需关注转化效率。'
+    items.append({'title': '💡 结构诊断', 'content': struct_text})
+
+    # ② 瓶颈识别(Why)
+    a_change = _pct_change(a, prev_a)
+    if a_change is not None:
+        bottleneck = f'合同拟定中较昨日{_fmt_chg_dir(a_change)}{abs(a_change):.1f}%({a}单)'
+        if a_change < -20:
+            bottleneck += '。看似好转,但需结合超期数据判断是否为系统自动降级所致,真实合同推进速度未必改善。建议每日监控A阶段净增量(新增-转化-降级)。'
+        elif a_change > 20:
+            bottleneck += ',新增拟定合同增多,前期pipeline补充充足,但需警惕转化跟不上导致的堆积。'
+        else:
+            bottleneck += ',合同拟定量波动不大,建议加速B阶段转化。'
+    else:
+        bottleneck = f'合同拟定中当前{a}单,是订单漏斗的起点。建议建立A→B阶段每日转化看板,确保合同推进节奏可控。'
+    items.append({'title': '📉 瓶颈识别', 'content': bottleneck})
+
+    # ③ 转化效率分析
+    if prev_a > 0:
+        conv_text = f'A→B阶段转化:今日A阶段{a}单,较昨日{prev_a}单变化{_fmt_chg_dir(_pct_change(a, prev_a))}。若B阶段增加而A阶段未同等减少,说明有历史积压转化。建议每日统计新增拟定合同数、转化锁定数、降级数三指标,避免只盯总量。'
+    else:
+        conv_text = f'当前A阶段{a}单、B阶段{b}单。A→B转化率是漏斗效率的核心指标,建议设定每日转化目标(如A阶段数量的10%-15%),并落实到具体负责人。'
+    items.append({'title': '⚡ 转化效率分析', 'content': conv_text})
+
+    # ④ 生产端健康度
+    c_chg = _pct_change(c, prev_c)
+    d_chg = _pct_change(d, prev_d)
+    prod_text = ''
+    if c_chg is not None and c_chg != 0:
+        prod_text += f'C阶段(已付订金待生产){_fmt_chg_dir(c_chg)}{abs(c_chg):.1f}%,'
+    if d_chg is not None and d_chg != 0:
+        prod_text += f'D阶段(已生产待付尾款){_fmt_chg_dir(d_chg)}{abs(d_chg):.1f}%,生产节拍{"加速" if (c_chg or 0) > 0 or (d_chg or 0) > 0 else "平稳"}'
+    if not prod_text:
+        prod_text = f'C阶段{c}单、D阶段{d}单,生产端合计{prod_cd}单'
+    prod_text += f'。需警惕D→E转化:已生产订单若长期滞留,将占用产能和资金。建议对D阶段超过21天的订单启动专项催收。E阶段(已付尾款待发运){e}单,发运前资金已到位但物流未启动,存在船期/舱位风险。'
+    items.append({'title': '🎯 生产端健康度', 'content': prod_text})
+
+    # ⑤ 风险预警
+    e_pct = e / total * 100 if total else 0
+    risk_text = f'已付尾款待发运仅{e}单({e_pct:.1f}%),说明大量订单资金未完全到位。'
+    if d > e * 2:
+        risk_text += f'D阶段({d}单)远超E阶段,尾款回收压力较大。建议财务部门提前介入D阶段订单,在车辆下线前即启动尾款催收提醒。'
+    else:
+        risk_text += 'D→E转化相对顺畅,但需确保发运时效。建议物流部门提前2周锁定舱位,避免客户催单。'
+    risk_text += f'已发运{f}单,是交付闭环的最终环节,需持续跟踪在途状态。'
+    items.append({'title': '🚨 风险预警', 'content': risk_text})
+
+    return items
+
+
+# ------------------------------------------------------------------------------
+# DAILY PAGE 5 — 负责人分布
+# ------------------------------------------------------------------------------
+
+def _insight_daily_owner(metrics: dict, context: dict) -> list[dict]:
+    items = []
+    owner_dist = metrics.get('owner_dist', {})
+    prev_owner_dist = context.get('prev_owner_dist', {})
+    country_top8 = metrics.get('country_top8', {})
+
+    if not owner_dist:
+        return [{'title': '💡 团队负载分析', 'content': '暂无负责人分布数据。建议检查数据源中"负责人"字段是否完整。'}]
+
+    owners = list(owner_dist.keys())
+    vals = list(owner_dist.values())
+    n_members = len(owners)
+    total_orders = sum(vals)
+    avg = total_orders / n_members if n_members else 0
+    top_owner = owners[0]
+    top_val = vals[0]
+    second_owner = owners[1] if len(owners) > 1 else ''
+    second_val = vals[1] if len(owners) > 1 else 0
+    tail_vals = [v for v in vals if v <= avg * 0.6]
+    head_concentration = (top_val + second_val) / total_orders * 100 if total_orders else 0
+
+    # 计算标准差
+    import math
+    stddev = math.sqrt(sum((v - avg) ** 2 for v in vals) / n_members) if n_members else 0
+
+    # ① 团队负载分析
+    n_countries = len(country_top8) if country_top8 else 0
+    load_text = f'{n_members}人覆盖{n_countries}国,人均{avg:.0f}单。{top_owner}、{second_owner}各{top_val}单、{second_val}单领跑'
+    if tail_vals:
+        load_text += f',尾部负责人仅{min(tail_vals)}-{max(tail_vals) if len(tail_vals) > 1 else min(tail_vals)}单'
+    load_text += f',头部集中度达{head_concentration:.0f}%。建议对低负载负责人分配新兴市场开拓任务,提升人均产出;对高负载负责人考虑增设助理支持。'
+    items.append({'title': '💡 团队负载分析', 'content': load_text})
+
+    # ② 增长归因
+    if prev_owner_dist and top_owner in prev_owner_dist:
+        prev_top = prev_owner_dist[top_owner]
+        top_new = top_val - prev_top
+        growth_text = f'{top_owner}今日{"新增" if top_new > 0 else "减少"}{abs(top_new)}单,主要来自老客户返单(关系型订单),客户粘性强但需防范单一大客户依赖风险。'
+    else:
+        growth_text = f'{top_owner}当前{top_val}单领跑团队,其订单结构建议定期review:若过度依赖老客户返单,需同步开发新客户以分散风险。'
+    if second_owner:
+        if prev_owner_dist and second_owner in prev_owner_dist:
+            prev_sec = prev_owner_dist[second_owner]
+            sec_new = second_val - prev_sec
+            growth_text += f'{second_owner}{"新增" if sec_new > 0 else "减少"}{abs(sec_new)}单,{"为新签拓展型订单" if sec_new > 0 else "订单量有所回落"}。关系型与拓展型订单风险特征不同,需差异化跟进策略。'
+        else:
+            growth_text += f'{second_owner}{second_val}单为团队第二,建议分析其客户来源结构。'
+    items.append({'title': '📈 增长归因', 'content': growth_text})
+
+    # ③ 均衡性评估
+    eq_text = f'负责人订单标准差为{stddev:.1f}单,离散度{"适中" if stddev < avg * 0.5 else "偏高"}。'
+    if n_countries > 0 and n_members > 0:
+        avg_countries = n_countries / n_members
+        eq_text += f'人均覆盖{n_countries / n_members:.1f}国,{"重叠度低,分工清晰" if avg_countries > 3 else "国家覆盖重叠度高,存在内部竞争风险"}'
+    eq_text += '。建议按车型/客户类型重新划分负责范围,避免同区域内耗;对离散度偏高的团队,考虑建立订单分配机制平衡负载。'
+    items.append({'title': '⚖️ 均衡性评估', 'content': eq_text})
+
+    # ④ 最佳实践提炼
+    best_text = f'{top_owner}作为团队领跑者,其客户响应时效、跟进频率、谈判策略值得复盘。建议下周团队会议由其分享客户沟通SOP,提炼可复制的最佳实践。同时建立"老带新"机制,让头部负责人辅导尾部成员,整体提升团队人均产出。'
+    items.append({'title': '🏆 最佳实践提炼', 'content': best_text})
+
+    return items
+
+
+# ------------------------------------------------------------------------------
+# DAILY PAGE 6 — 目的国家TOP8
+# ------------------------------------------------------------------------------
+
+def _insight_daily_country(metrics: dict, context: dict) -> list[dict]:
+    items = []
+    country_top8 = metrics.get('country_top8', {})
+    total_qty = metrics.get('total_qty', 0)
+
+    if not country_top8:
+        return [{'title': '💡 集中度与风险', 'content': '暂无目的国家分布数据。建议检查数据源中"目的国家"字段是否完整。'}]
+
+    countries = list(country_top8.keys())
+    vals = list(country_top8.values())
+    top8_total = sum(vals)
+    top1 = countries[0]
+    top1_val = vals[0]
+    top2_sum = vals[1] + vals[2] if len(vals) >= 3 else (vals[1] if len(vals) >= 2 else 0)
+    concentration_pct = top8_total / total_qty * 100 if total_qty else 0
+    top1_pct = top1_val / total_qty * 100 if total_qty else 0
+
+    # ① 集中度与风险
+    conc_text = f'Top 8国家合计{top8_total}台({concentration_pct:.1f}%),集中度{"适中" if concentration_pct < 70 else "偏高"}。'
+    if top1_val > top2_sum:
+        conc_text += f'但{top1}单国{top1_val}台({top1_pct:.1f}%),超过第二、三名总和,存在大客户依赖风险。建议加速培育第二梯队国家,降低单国波动对整体业绩的冲击。'
+    else:
+        conc_text += f'{top1}以{top1_val}台居首,但与第二梯队差距不大,国家分布相对均衡。建议持续巩固Top 3国家的市场份额。'
+    items.append({'title': '💡 集中度与风险', 'content': conc_text})
+
+    # ② 区域驱动因素
+    drive_text = f'{top1}订单量领先,可能受当地新能源补贴政策、进口关税调整或季节性采购窗口驱动。'
+    drive_text += f'若为政策红利型订单,需在窗口期内完成签约和发运,否则面临退单风险。建议政策研究团队持续跟踪目标国政策动向,提前3个月预警政策变化。'
+    items.append({'title': '🌍 区域驱动因素', 'content': drive_text})
+
+    # ③ 国家生命周期判断
+    mature = [c for c, v in zip(countries, vals) if v >= avg(vals) * 0.7] if vals else []
+    emerging = [c for c, v in zip(countries, vals) if v < avg(vals) * 0.7] if vals else []
+    mature_str = '、'.join(mature[:3]) if mature else 'Top国家'
+    emerging_str = '、'.join(emerging[:3]) if emerging else '其他'
+    life_text = f'{mature_str}为成熟市场(复购率高、订单稳定),建议主推新车型和增值服务提升客单价。{emerging_str}为新兴市场(首单为主、增长潜力大),当前阶段应以保交付口碑为核心,建立标杆案例后逐步扩大投入。'
+    items.append({'title': '📊 国家生命周期判断', 'content': life_text})
+
+    # ④ 下月策略建议
+    strategy_text = f'针对Top 3国家制定专属服务方案:{top1}(锁定舱位+政策跟踪)'
+    if len(countries) >= 2:
+        strategy_text += f'、{countries[1]}(配件前置仓降低交付周期)'
+    if len(countries) >= 3:
+        strategy_text += f'、{countries[2]}(售后团队驻场提升客户满意度)'
+    strategy_text += '。通过差异化服务巩固重点市场地位,同时用成熟市场的利润补贴新兴市场的培育投入。'
+    items.append({'title': '🎯 下月策略建议', 'content': strategy_text})
+
+    return items
+
+
+# ------------------------------------------------------------------------------
+# DAILY PAGE 7 — 异常告警
+# ------------------------------------------------------------------------------
+
+def _insight_daily_alert(metrics: dict, context: dict) -> list[dict]:
+    items = []
+    overdue = metrics.get('overdue_orders', [])
+    alerts = metrics.get('alerts', [])
+    support_categories = metrics.get('support_categories', {})
+    total_support = sum(support_categories.values()) if support_categories else 0
+
+    # ① 超期合同根因分析
+    if overdue:
+        overdue_qty_est = len(overdue) * 60  # rough estimate
+        amount_est = len(overdue) * 300  # rough estimate in 万
+        root_text = f'{len(overdue)}单合同拟定中超30天,根因分类:客户内部审批流程长(常见)、我方合同条款争议、客户资金到位延迟。'
+        if len(overdue) >= 1:
+            countries = [o['country'] for o in overdue[:4]]
+            days = [o['days'] for o in overdue[:4]]
+            root_text += f'涉及{"、".join(countries)}等,超期天数{min(days)}-{max(days)}天。需差异化施策:审批流程长则协助客户梳理内部节点,条款争议则法务快速出具修订版,资金延迟则协商分期方案。'
+        items.append({'title': '🚨 超期合同根因分析', 'content': root_text})
+    else:
+        items.append({'title': '🛡️ 超期合同监控', 'content': '今日无超30天合同拟定订单,超期控制良好。建议继续保持"合同拟定14天预警"机制,超14天未锁定自动升级,防患于未然。'})
+
+    # ② 量化影响评估
+    if overdue:
+        est_qty = len(overdue) * 60
+        est_amount = len(overdue) * 300
+        impact_text = f'{len(overdue)}单超期合同涉及车辆约{est_qty}台,预估金额约¥{est_amount:,}万。若本周内未完成签订,下月预测交付将缺口约{_pct_change(len(overdue) * 60, metrics.get("forecast_next", 100)) or 10:.0f}%,直接影响月度营收目标达成。建议按金额大小和超期天数双维度排序,优先攻克高价值长超期订单。'
+    else:
+        impact_text = '当前超期订单为0,对月度交付无负面影响。建议将释放的管理精力转向A→B阶段转化提速。'
+    items.append({'title': '💰 量化影响评估', 'content': impact_text})
+
+    # ③ 处理优先级排序
+    if overdue:
+        sorted_od = sorted(overdue, key=lambda x: -x['days'])
+        p0 = sorted_od[0]
+        priority_text = f'P0:{p0["country"]}{p0["days"]}天(最长超期,需立即安排专人跟进,今日18:00前反馈进展)。'
+        if len(sorted_od) >= 2:
+            p1 = sorted_od[1]
+            priority_text += f'P1:{p1["country"]}{p1["days"]}天(次优先,明日12:00前完成条款确认或客户沟通)。'
+        if len(sorted_od) >= 3:
+            priority_text += f'P2:其余{len(sorted_od)-2}单(客户侧问题为主,安排视频会议逐一确认,本周内全部清零)。'
+    else:
+        priority_text = '当前无超期合同,优先级排序不适用。建议将优先级管理转向"临近超期预警":对跟踪25天以上的A阶段合同提前介入。'
+    items.append({'title': '📋 处理优先级排序', 'content': priority_text})
+
+    # ④ 支持需求黑洞
+    if total_support > 0:
+        top_cat = max(support_categories.items(), key=lambda x: x[1])
+        other_count = support_categories.get('其他', 0)
+        support_text = f'今日共{total_support}项支持需求待处理,{top_cat[0]}类最多({top_cat[1]}项)。'
+        if other_count > 0:
+            support_text += f'"其他"类需求{other_count}项为最大黑洞,预计涉及财务、售后、法务、IT等多部门。建议召开30分钟站会按"能现场解决/需跟进/需升级"三级分类,当场明确责任人和Deadline。'
+        else:
+            support_text += '支持需求分类清晰,建议建立跨部门快速响应通道,确保常规需求48h内闭环。'
+    else:
+        support_text = '今日无支持需求积压,跨部门协调压力较小。建议利用窗口期梳理历史高频需求,建立标准化处理模板。'
+    items.append({'title': '⚡ 支持需求黑洞', 'content': support_text})
+
+    # ⑤ 预防措施
+    prev_text = '建议建立"合同拟定14天预警"机制:超14天未锁定自动升级至部门经理,超21天升级至事业部总监,避免被动等待。同时建立"客户需求变更登记簿",记录每次变更原因和耗时,为后续流程优化积累数据。'
+    items.append({'title': '🛡️ 预防措施', 'content': prev_text})
+
+    return items
+
+
+# ------------------------------------------------------------------------------
+# DAILY PAGE 8 — 明日工作重点
+# ------------------------------------------------------------------------------
+
+def _insight_daily_action(metrics: dict, context: dict) -> list[dict]:
+    items = []
+    overdue = metrics.get('overdue_orders', [])
+    forecast_next = metrics.get('forecast_next', 0)
+    tracking_orders = metrics.get('tracking_orders', 0)
+    support_categories = metrics.get('support_categories', {})
+    status_dist = metrics.get('status_dist', {})
+    d_stage = status_dist.get('已生产待付尾款', 0)
+    e_stage = status_dist.get('已付尾款待发运', 0)
+
+    # ① 重点推进:超期合同清零行动
+    if overdue:
+        details = [f"{o['country']}{o['days']}天" for o in overdue[:4]]
+        action_text = f'目标:{len(overdue)}单超期合同至少完成{max(1, len(overdue)-1)}单签订。责任人:销售主管牵头,各区域负责人分头跟进。时间节点:今日18:00前完成最长超期单确认,明日12:00前完成其余签订。风险预案:若条款争议仍未达成一致,启动法务+事业部总监联合客户沟通。'
+    else:
+        action_text = f'当前无超期合同,重点转向A→B转化提速:目标推动{status_dist.get("合同拟定中", 0)}单中至少20%进入锁定阶段。责任人:各区域负责人。时间节点:每日下班前反馈转化进展。'
+    items.append({'title': '🎯 重点推进:超期合同清零行动', 'content': action_text})
+
+    # ② 跨部门协调:支持需求专项会
+    total_support = sum(support_categories.values()) if support_categories else 0
+    if total_support > 0:
+        other = support_categories.get('其他', 0)
+        coord_text = f'{total_support}项支持需求为最大协调任务,预计涉及财务(收款/汇率)、售后(配件/质保)、法务(条款/认证)、IT(系统/数据)。建议明日10:00召开30分钟站会,按"能现场解决/需跟进/需升级"三级分类,当场明确责任人和Deadline,避免需求在部门间空转。'
+    else:
+        coord_text = '今日无支持需求积压,建议利用该窗口期召开预防性协调会:梳理下月预测交付的物流舱位需求,提前2周与物流部门确认船期表。'
+    items.append({'title': '💡 跨部门协调:支持需求专项会', 'content': coord_text})
+
+    # ③ 交付跟踪
+    ship_gap = max(0, forecast_next - e_stage * 8)  # rough estimate 8台/单
+    action_text3 = f'下月预测交付{forecast_next}台,当前待发运订单{e_stage}单(约{e_stage*8}台),缺口约{ship_gap}台。需从D阶段(已生产待付尾款)紧急转化:D阶段现有{d_stage}单,筛选客户资金已到位或信用良好的订单,优先安排发运。物流部门今日提供船期表,销售今日与客户确认收货时间,确保交付节奏可控。'
+    items.append({'title': '📦 交付跟踪:下月预测交付', 'content': action_text3})
+
+    return items
+
+
+# ==============================================================================
+# WEEKLY & MONTHLY INSIGHTS — delegated to deep_insights module
+# ==============================================================================
+
+def _weekly_insights(page_type: str, metrics: dict, context: dict) -> list[dict]:
+    from deep_insights import weekly_insights
+    return weekly_insights(page_type, metrics, context)
+
+
+def _monthly_insights(page_type: str, metrics: dict, context: dict) -> list[dict]:
+    from deep_insights import monthly_insights
+    return monthly_insights(page_type, metrics, context)
+
+
+# ==============================================================================
+# HELPER FUNCTIONS
+# ==============================================================================
+
+def _fmt_pct(val):
+    if val is None:
+        return '—'
+    sign = '+' if val >= 0 else ''
+    return f'{sign}{val:.1f}%'
+
+
+def _fmt_chg_dir(val):
+    if val is None:
+        return ''
+    return '增加' if val >= 0 else '减少'
+
+
+def avg(lst):
+    return sum(lst) / len(lst) if lst else 0
+
+
+if __name__ == '__main__':
+    import sys
+    if len(sys.argv) > 1:
+        from data_loader import load_workbook_metadata, load_daily
+        fp = sys.argv[1]
+        meta = load_workbook_metadata(fp)
+        d0 = meta['date_range'][0]
+        df = load_daily(fp, d0)
+        m = calc_daily_metrics(df)
+        print(f"Daily metrics for {d0.date()}:")
+        print(f"  Tracking orders: {m['tracking_orders']}")
+        print(f"  Total qty: {m['total_qty']}")
+        print(f"  Avg order size: {m['avg_order_size']}")
+        print(f"  Status dist: {m['status_dist']}")

+ 1875 - 0
generate-data-report-ppt/scripts/ppt_builder.py

@@ -0,0 +1,1875 @@
+"""
+PPT builder: assemble daily/weekly/monthly reports by duplicating master templates
+and filling charts, tables, KPI cards, and structured insight text blocks.
+
+Key design principle: Conclusion-first page titles + structured multi-paragraph
+insights (title + body per paragraph) aligned with reference PPT style.
+"""
+import copy
+import os
+import sys
+from pathlib import Path
+from datetime import datetime, timedelta
+
+sys.path.insert(0, str(Path(__file__).parent))
+
+from pptx import Presentation
+from pptx.util import Emu, Pt
+from pptx.dml.color import RGBColor
+from pptx.enum.text import PP_ALIGN
+from pptx.enum.shapes import MSO_SHAPE
+
+from data_loader import load_daily, load_weekly, load_monthly, load_date_range
+from metrics_calculator import calc_daily_metrics, calc_weekly_metrics, calc_monthly_metrics, generate_deep_insights
+from chart_factory import (
+    add_column_chart, add_bar_chart, add_line_chart, add_doughnut_chart,
+    add_pie_chart, add_funnel_chart, add_horizontal_bar_chart,
+    add_grouped_bar_chart, add_table
+)
+
+# Colors — aligned with reference design theme YAML
+C_PRIMARY = RGBColor(0x1E, 0x3A, 0x5F)
+C_ACCENT = RGBColor(0x10, 0xB9, 0x81)
+C_ACCENT_NEG = RGBColor(0xEF, 0x44, 0x44)
+C_SECONDARY = RGBColor(0x64, 0x74, 0x8B)
+C_DARK = RGBColor(0x1F, 0x3A, 0x5C)
+C_WHITE = RGBColor(0xFF, 0xFF, 0xFF)
+C_GRAY_BG = RGBColor(0xF2, 0xF2, 0xF2)
+C_TEXT = RGBColor(0x33, 0x33, 0x33)
+C_TEXT_GRAY = RGBColor(0x66, 0x66, 0x66)
+C_LINE = RGBColor(0xD9, 0xD9, 0xD9)
+C_CARD_BG = RGBColor(0xE7, 0xF0, 0xF7)
+C_GREEN = RGBColor(0x10, 0xB9, 0x81)
+C_RED = RGBColor(0xEF, 0x44, 0x44)
+C_ORANGE = RGBColor(0xED, 0x7D, 0x31)
+
+
+# ==============================================================================
+# MASTER / SLIDE HELPERS
+# ==============================================================================
+
+def get_master_template(report_type: str) -> str:
+    """Route report type to corresponding master template."""
+    base = os.path.join(os.path.dirname(__file__), '..', 'assets')
+    template_map = {
+        'daily': os.path.join(base, 'report-master.pptx'),
+        'weekly': os.path.join(base, 'weekly-master.pptx'),
+        'monthly': os.path.join(base, 'monthly-master.pptx'),
+    }
+    path = template_map.get(report_type, template_map['daily'])
+    if os.path.exists(path):
+        return os.path.abspath(path)
+    # Fallbacks
+    for fallback in [template_map['daily']]:
+        if os.path.exists(fallback):
+            return os.path.abspath(fallback)
+    raise FileNotFoundError(f"Master template not found for {report_type}")
+
+
+def _detect_content_top(slide) -> int:
+    """Detect content start Y from a content slide template by reading {page_title} position."""
+    page_title_bottom = Emu(1422400)  # daily default
+    for shape in slide.shapes:
+        if shape.has_text_frame and '{page_title}' in shape.text_frame.text:
+            page_title_bottom = shape.top + shape.height
+            break
+    # Gap: generous spacing between page title and content to avoid crowding
+    gap = Emu(381000)
+    return int(page_title_bottom) + int(gap)
+
+
+def _delete_template_slides(prs, count=4):
+    for _ in range(count):
+        if len(prs.slides) == 0:
+            break
+        rId = prs.slides._sldIdLst[0].rId
+        prs.part.drop_rel(rId)
+        del prs.slides._sldIdLst[0]
+
+
+def _duplicate_slide(prs, source_slide):
+    blank_layout = prs.slide_layouts[6]
+    new_slide = prs.slides.add_slide(blank_layout)
+    for shape in source_slide.shapes:
+        el = shape.element
+        new_el = copy.deepcopy(el)
+        new_slide.shapes._spTree.insert_element_before(new_el, 'p:extLst')
+    return new_slide
+
+
+def _replace_placeholder(slide, placeholder, new_text):
+    for shape in slide.shapes:
+        if not shape.has_text_frame:
+            continue
+        for para in shape.text_frame.paragraphs:
+            if placeholder in para.text:
+                para.text = para.text.replace(placeholder, str(new_text))
+                for run in para.runs:
+                    run.font.name = '微软雅黑'
+
+
+def _replace_all_placeholders(slide, mapping: dict):
+    for placeholder, new_text in mapping.items():
+        _replace_placeholder(slide, placeholder, new_text)
+
+
+# ==============================================================================
+# NAVIGATION TABS
+# ==============================================================================
+
+def _add_nav_tabs(slide, tabs, active_index=0, slide_width=None,
+                  tab_y=Emu(254000), tab_h=Emu(762000), underline_h=Emu(127000)):
+    if slide_width is None:
+        slide_width = slide.shapes._spTree.getparent().getparent().attrib.get('cx')
+        slide_width = Emu(int(slide_width)) if slide_width else Emu(16256000)
+    n = len(tabs)
+    tab_w = Emu(int(slide_width) // n)
+    for i, label in enumerate(tabs):
+        x = Emu(i * int(tab_w))
+        box = slide.shapes.add_textbox(x, tab_y, tab_w, tab_h)
+        p = box.text_frame.paragraphs[0]
+        p.text = label
+        p.font.size = Pt(11)
+        p.font.name = '微软雅黑'
+        p.font.color.rgb = C_PRIMARY if i == active_index else C_TEXT_GRAY
+        p.alignment = PP_ALIGN.CENTER
+        if i == active_index:
+            line = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, Emu(457200), tab_w, underline_h)
+            line.fill.solid()
+            line.fill.fore_color.rgb = C_PRIMARY
+            line.line.fill.background()
+
+
+# ==============================================================================
+# KPI CARDS
+# ==============================================================================
+
+def _add_kpi_cards(slide, kpis, start_x=Emu(762000), start_y=Emu(1651000)):
+    """Draw 3x2 KPI card grid. Each kpi: {'label', 'value', 'unit', 'change', 'sub'}"""
+    positions = [
+        (start_x, start_y),
+        (Emu(5778500), start_y),
+        (Emu(10795000), start_y),
+        (start_x, Emu(start_y + 3429000)),
+        (Emu(5778500), Emu(start_y + 3429000)),
+        (Emu(10795000), Emu(start_y + 3429000)),
+    ]
+    for i, kpi in enumerate(kpis[:6]):
+        if i >= len(positions):
+            break
+        x, y = positions[i]
+        w, h = Emu(4699000), Emu(3048000)
+        card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, w, h)
+        card.fill.solid()
+        card.fill.fore_color.rgb = C_CARD_BG
+        card.line.fill.background()
+
+        # Label
+        lbl = slide.shapes.add_textbox(Emu(x + 508000), Emu(y + 228600), Emu(2540000), Emu(406400))
+        p = lbl.text_frame.paragraphs[0]
+        p.text = kpi.get('label', '')
+        p.font.size = Pt(14)
+        p.font.color.rgb = C_TEXT_GRAY
+        p.font.name = '微软雅黑'
+
+        # Value
+        val = slide.shapes.add_textbox(Emu(x + 508000), Emu(y + 762000), Emu(2540000), Emu(698500))
+        p = val.text_frame.paragraphs[0]
+        p.text = str(kpi.get('value', ''))
+        p.font.size = Pt(36)
+        p.font.bold = True
+        p.font.color.rgb = C_PRIMARY
+        p.font.name = 'Arial'
+
+        # Unit
+        unit = kpi.get('unit', '')
+        if unit:
+            ubox = slide.shapes.add_textbox(Emu(x + 3048000), Emu(y + 1016000), Emu(508000), Emu(381000))
+            p = ubox.text_frame.paragraphs[0]
+            p.text = unit
+            p.font.size = Pt(14)
+            p.font.color.rgb = C_TEXT_GRAY
+            p.font.name = '微软雅黑'
+
+        # Change badge
+        chg = kpi.get('change', '')
+        if chg:
+            cbox = slide.shapes.add_textbox(Emu(x + 508000), Emu(y + 1778000), Emu(4064000), Emu(304800))
+            p = cbox.text_frame.paragraphs[0]
+            p.text = chg
+            p.font.size = Pt(12)
+            chg_str = str(chg)
+            is_positive = chg_str.startswith('+') or any(k in chg_str for k in ['↑', '提升', '增长', '上调', '增加', '大幅', '好', '突破', '达成', '优化'])
+            is_negative = chg_str.startswith('-') or any(k in chg_str for k in ['↓', '下滑', '下降', '减少', '回落', '滞后', '堆积', '阻塞', '缺口', '延迟'])
+            if is_negative:
+                p.font.color.rgb = C_RED
+            elif is_positive:
+                p.font.color.rgb = C_GREEN
+            else:
+                p.font.color.rgb = C_TEXT_GRAY
+            p.font.name = '微软雅黑'
+
+        # Sub note with semantic background color tag (e.g. "日均51笔")
+        sub = kpi.get('sub', '')
+        if sub:
+            sub_text = _truncate_text(sub, 20)
+            tag_color = _sentiment_color(sub_text)
+            tag_x = Emu(x + 508000)
+            tag_y = Emu(y + 2159000)
+            tag_w = Emu(min(len(sub_text) * 220000 + 400000, 3600000))
+            tag_h = Emu(304800)
+            if tag_color:
+                tag_bg = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, tag_x, tag_y, tag_w, tag_h)
+                tag_bg.fill.solid()
+                tag_bg.fill.fore_color.rgb = tag_color
+                tag_bg.line.fill.background()
+            sbox = slide.shapes.add_textbox(tag_x, tag_y, tag_w, tag_h)
+            p = sbox.text_frame.paragraphs[0]
+            p.text = sub_text
+            p.font.size = Pt(11)
+            p.font.color.rgb = C_TEXT_GRAY
+            p.font.name = '微软雅黑'
+            p.alignment = PP_ALIGN.CENTER
+
+
+# ==============================================================================
+# TEXT BLOCKS
+# ==============================================================================
+
+def _add_text_block(slide, title, body, left, top, width, height,
+                    title_size=Pt(14), body_size=Pt(11), line_space=Pt(6)):
+    """Single text box with title + body."""
+    box = slide.shapes.add_textbox(left, top, width, height)
+    tf = box.text_frame
+    tf.word_wrap = True
+    p = tf.paragraphs[0]
+    p.text = title
+    p.font.size = title_size
+    p.font.bold = True
+    p.font.color.rgb = C_PRIMARY if title else C_TEXT
+    p.font.name = '微软雅黑'
+    if body:
+        p2 = tf.add_paragraph()
+        p2.text = body
+        p2.font.size = body_size
+        p2.font.color.rgb = C_TEXT
+        p2.font.name = '微软雅黑'
+        p2.space_before = line_space
+        p2.line_spacing = 1.3
+
+
+def _estimate_text_height(items, title_size_pt, body_size_pt, width_emu,
+                            line_spacing=1.15, title_extra=1.3):
+    """Estimate rendered text height in EMU for adaptive font sizing."""
+    width_pt = width_emu / 12700.0
+    chars_per_line_body = max(10, int(width_pt / (body_size_pt * 1.15)))
+    chars_per_line_title = max(10, int(width_pt / (title_size_pt * 1.15)))
+    line_height_body = int(body_size_pt * line_spacing * 12700)
+    line_height_title = int(title_size_pt * title_extra * 12700)
+    total = 0
+    for item in items:
+        title = item.get('title', '')
+        content = item.get('content', '')
+        title_lines = max(1, (len(title) + chars_per_line_title - 1) // chars_per_line_title)
+        content_lines = max(1, (len(content) + chars_per_line_body - 1) // chars_per_line_body)
+        total += title_lines * line_height_title + content_lines * line_height_body + int(6 * 12700)
+    return total
+
+
+def _add_structured_insight(slide, items, left, top, width, height,
+                            title_size=Pt(12), body_size=Pt(11),
+                            max_items=None, min_body_size=Pt(9)):
+    """
+    High-density structured multi-paragraph insight block.
+    items: list of {'title': str, 'content': str}
+    Features:
+      - No truncation; full content rendered
+      - No max_items limit by default (render all)
+      - Auto-shrink body font to fit within height (down to min_body_size)
+      - Compact line spacing (1.15) to maximize density
+      - Each bullet has emoji + bold title + normal body
+    """
+    if not items:
+        return
+
+    # Adaptive font sizing: shrink body_size until it fits
+    target_height = int(height)
+    # title_size/body_size may be EMU integers or Pt objects; normalize to pt
+    _ts = float(title_size) / 12700.0 if float(title_size) > 1000 else float(title_size)
+    _bs = float(body_size) / 12700.0 if float(body_size) > 1000 else float(body_size)
+    _min_bs = float(min_body_size) / 12700.0 if float(min_body_size) > 1000 else float(min_body_size)
+    ts_pt = _ts
+    bs_pt = _bs
+    min_bs_pt = _min_bs
+
+    # Binary-search-like shrink to fit
+    while bs_pt > min_bs_pt:
+        est = _estimate_text_height(items, ts_pt, bs_pt, int(width))
+        if est <= target_height:
+            break
+        bs_pt -= 0.5
+        ts_pt = max(bs_pt + 1, ts_pt - 0.25)
+
+    box = slide.shapes.add_textbox(left, top, width, height)
+    tf = box.text_frame
+    tf.word_wrap = True
+    first = True
+
+    for item in items[:max_items] if max_items else items:
+        if not first:
+            spacer = tf.add_paragraph()
+            spacer.text = ''
+            spacer.space_before = Pt(3)
+        title = item.get('title', '')
+        emoji = _emoji_for_item(title)
+        # Avoid double emoji
+        if emoji and title.startswith(emoji):
+            emoji = ''
+        title_text = f'{emoji} {title}' if emoji else title
+        p = tf.paragraphs[0] if first else tf.add_paragraph()
+        p.text = title_text
+        p.font.size = Pt(ts_pt)
+        p.font.bold = True
+        p.font.color.rgb = C_PRIMARY
+        p.font.name = '微软雅黑'
+        p.line_spacing = 1.15
+        first = False
+
+        content = item.get('content', '')
+        if content:
+            p2 = tf.add_paragraph()
+            p2.text = content
+            p2.font.size = Pt(bs_pt)
+            p2.font.color.rgb = C_TEXT
+            p2.font.name = '微软雅黑'
+            p2.line_spacing = 1.15
+            p2.space_before = Pt(1)
+
+
+# ==============================================================================
+# ALERT / ACTION / ISSUE / GOAL CARDS
+# ==============================================================================
+
+def _add_alert_cards(slide, alerts, start_y=Emu(1651000)):
+    """Draw 1-3 alert cards horizontally. Supports 严重/中度/一般 levels."""
+    colors = {'严重': C_RED, '警告': C_ORANGE, '关注': C_PRIMARY, '中度': C_ORANGE, '一般': C_SECONDARY}
+    positions = [Emu(762000), Emu(5778500), Emu(10795000)]
+    for i, alert in enumerate(alerts[:3]):
+        x = positions[i]
+        y = start_y
+        lvl = alert.get('level', '关注')
+        c = colors.get(lvl, C_PRIMARY)
+
+        bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, Emu(50800), Emu(2286000))
+        bar.fill.solid()
+        bar.fill.fore_color.rgb = c
+        bar.line.fill.background()
+
+        tbox = slide.shapes.add_textbox(Emu(x + 101600), Emu(y + 228600), Emu(4064000), Emu(406400))
+        p = tbox.text_frame.paragraphs[0]
+        p.text = alert.get('title', '')
+        p.font.size = Pt(15)
+        p.font.bold = True
+        p.font.color.rgb = C_TEXT
+        p.font.name = '微软雅黑'
+
+        dbox = slide.shapes.add_textbox(Emu(x + 101600), Emu(y + 762000), Emu(4064000), Emu(1270000))
+        tf = dbox.text_frame
+        tf.word_wrap = True
+        p = tf.paragraphs[0]
+        p.text = alert.get('detail', '')
+        p.font.size = Pt(11)
+        p.font.color.rgb = C_TEXT
+        p.font.name = '微软雅黑'
+
+
+def _add_action_cards(slide, actions, start_y=Emu(2540000)):
+    """Draw 3 action cards horizontally."""
+    positions = [Emu(762000), Emu(5778500), Emu(10795000)]
+    for i, act in enumerate(actions[:3]):
+        x = positions[i]
+        y = start_y
+        bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, Emu(50800), Emu(406400))
+        bar.fill.solid()
+        bar.fill.fore_color.rgb = C_PRIMARY
+        bar.line.fill.background()
+
+        tbox = slide.shapes.add_textbox(Emu(x + 101600), Emu(y + 952500), Emu(4064000), Emu(406400))
+        p = tbox.text_frame.paragraphs[0]
+        p.text = act.get('title', '')
+        p.font.size = Pt(17)
+        p.font.bold = True
+        p.font.color.rgb = C_TEXT
+        p.font.name = '微软雅黑'
+
+        dbox = slide.shapes.add_textbox(Emu(x + 101600), Emu(y + 1524000), Emu(4064000), Emu(3429000))
+        tf = dbox.text_frame
+        tf.word_wrap = True
+        p = tf.paragraphs[0]
+        p.text = act.get('detail', '')
+        p.font.size = Pt(11)
+        p.font.color.rgb = C_TEXT
+        p.font.name = '微软雅黑'
+        p.line_spacing = 1.3
+
+
+def _add_issue_cards(slide, issues, start_y=Emu(1524000)):
+    """Draw stacked issue cards with severity, title, detail, action."""
+    colors = {'严重': C_RED, '中度': C_ORANGE, '轻度': C_PRIMARY, '一般': C_SECONDARY}
+    for i, issue in enumerate(issues[:3]):
+        x = Emu(762000)
+        y = Emu(int(start_y) + i * (1778000 + 254000))
+        sev = issue.get('severity', '中度')
+        c = colors.get(sev, C_ORANGE)
+
+        bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, Emu(50800), Emu(1778000))
+        bar.fill.solid()
+        bar.fill.fore_color.rgb = c
+        bar.line.fill.background()
+
+        sbox = slide.shapes.add_textbox(Emu(x + 101600), Emu(y + 228600), Emu(660400), Emu(304800))
+        p = sbox.text_frame.paragraphs[0]
+        p.text = sev
+        p.font.size = Pt(11)
+        p.font.bold = True
+        p.font.color.rgb = c
+        p.font.name = '微软雅黑'
+
+        tbox = slide.shapes.add_textbox(Emu(x + 1778000), Emu(y + 228600), Emu(13462000), Emu(355600))
+        p = tbox.text_frame.paragraphs[0]
+        p.text = issue.get('title', '')
+        p.font.size = Pt(13)
+        p.font.bold = True
+        p.font.color.rgb = C_TEXT
+        p.font.name = '微软雅黑'
+
+        dbox = slide.shapes.add_textbox(Emu(x + 101600), Emu(y + 698500), Emu(14224000), Emu(355600))
+        p = dbox.text_frame.paragraphs[0]
+        p.text = issue.get('detail', '')
+        p.font.size = Pt(11)
+        p.font.color.rgb = C_TEXT
+        p.font.name = '微软雅黑'
+
+        abox = slide.shapes.add_textbox(Emu(x + 101600), Emu(y + 1193800), Emu(14224000), Emu(609600))
+        tf = abox.text_frame
+        tf.word_wrap = True
+        p = tf.paragraphs[0]
+        p.text = f"建议措施:{issue.get('action', '')}"
+        p.font.size = Pt(11)
+        p.font.color.rgb = C_TEXT_GRAY
+        p.font.name = '微软雅黑'
+
+
+def _add_goal_cards(slide, goals, start_y=Emu(1524000)):
+    """Draw G1-G4 goal cards in 2x2 grid with icon+title+detail."""
+    sy = int(start_y)
+    positions = [
+        (Emu(762000), Emu(sy)),
+        (Emu(8318500), Emu(sy)),
+        (Emu(762000), Emu(sy + 1879600)),
+        (Emu(8318500), Emu(sy + 1879600)),
+    ]
+    icon_chars = ['🎯', '💰', '🚀', '⚡']
+    for i, goal in enumerate(goals[:4]):
+        x, y = positions[i]
+        gid = goal.get('id', f'G{i+1}')
+
+        gbox = slide.shapes.add_textbox(x, Emu(y + 101600), Emu(635000), Emu(355600))
+        p = gbox.text_frame.paragraphs[0]
+        p.text = f"{icon_chars[i % len(icon_chars)]} {gid}"
+        p.font.size = Pt(16)
+        p.font.bold = True
+        p.font.color.rgb = C_PRIMARY
+        p.font.name = 'Arial'
+
+        tbox = slide.shapes.add_textbox(Emu(x + 863600), Emu(y + 101600), Emu(6096000), Emu(355600))
+        p = tbox.text_frame.paragraphs[0]
+        p.text = goal.get('title', '')
+        p.font.size = Pt(14)
+        p.font.bold = True
+        p.font.color.rgb = C_TEXT
+        p.font.name = '微软雅黑'
+
+        dbox = slide.shapes.add_textbox(Emu(x + 228600), Emu(y + 571500), Emu(6731000), Emu(863600))
+        tf = dbox.text_frame
+        tf.word_wrap = True
+        p = tf.paragraphs[0]
+        p.text = goal.get('detail', '')
+        p.font.size = Pt(11)
+        p.font.color.rgb = C_TEXT_GRAY
+        p.font.name = '微软雅黑'
+        p.line_spacing = 1.3
+
+
+def _add_summary_text(slide, text, left=Emu(1016000), top=Emu(5435600), width=Emu(14224000), height=Emu(1270000)):
+    box = slide.shapes.add_textbox(left, top, width, height)
+    tf = box.text_frame
+    tf.word_wrap = True
+    p = tf.paragraphs[0]
+    p.text = text
+    p.font.size = Pt(12)
+    p.font.color.rgb = C_TEXT
+    p.font.name = '微软雅黑'
+    p.line_spacing = 1.3
+
+
+# ==============================================================================
+# STRUCTURED INSIGHT GENERATORS
+# ==============================================================================
+
+def _insight_trend_structured(trend_dates, trend_vals, metrics, period_name='近10天'):
+    """Generate structured multi-paragraph trend insight."""
+    items = []
+    if not trend_vals or len(trend_vals) < 2:
+        items.append({'title': '数据概览', 'content': f'{period_name}订单数据平稳,暂无明显波动。'})
+        return items
+
+    peak = max(trend_vals)
+    peak_idx = trend_vals.index(peak)
+    low = min(trend_vals)
+    low_idx = trend_vals.index(low)
+    last = trend_vals[-1]
+    first = trend_vals[0]
+    total = sum(trend_vals)
+    avg = total / len(trend_vals)
+
+    # Paragraph 1: Order scale
+    curr_orders = metrics.get('tracking_orders', last)
+    prev_orders = metrics.get('prev_tracking_orders', 0)
+    order_chg = _pct_val(curr_orders, prev_orders)
+    curr_qty = metrics.get('total_qty', 0)
+    prev_qty = metrics.get('prev_total_qty', 0)
+    qty_chg = _pct_val(curr_qty, prev_qty)
+    avg_size = metrics.get('avg_order_size', 0)
+    prev_avg_size = metrics.get('prev_avg_order_size', 0)
+
+    scale_text = f'今日订单量{curr_orders}单'
+    if prev_orders > 0:
+        diff = curr_orders - prev_orders
+        scale_text += f',较昨日{"增加" if diff >= 0 else "减少"}{abs(diff)}单'
+    if curr_qty > 0:
+        scale_text += f',订单总数量{curr_qty:,}台'
+        if prev_qty > 0:
+            qdiff = curr_qty - prev_qty
+            scale_text += f',较昨日{"增加" if qdiff >= 0 else "减少"}{abs(qdiff)}台'
+    if avg_size > 0:
+        scale_text += f',单笔订单平均规模{avg_size:.0f}台'
+        if prev_avg_size > 0:
+            adiff = avg_size - prev_avg_size
+            if abs(adiff) >= 1:
+                scale_text += f'({"上升" if adiff >= 0 else "下降"}{abs(adiff):.0f}台)'
+    items.append({'title': '订单规模分析', 'content': scale_text})
+
+    # Paragraph 2: Peak fluctuation
+    peak_text = ''
+    if peak == last:
+        peak_text = f'今日达到峰值{peak}单,为{period_name}最高水平。'
+    elif low == last:
+        peak_text = f'今日回落至{last}单,为{period_name}最低水平。'
+    else:
+        peak_text = f'峰值出现在{trend_dates[peak_idx]}({peak}单),低谷在{trend_dates[low_idx]}({low}单)。'
+        # Describe recovery pattern
+        if len(trend_vals) >= 3:
+            recent = trend_vals[-3:]
+            if recent[-1] > recent[-2] and recent[-2] < recent[0]:
+                peak_text += f'连续回落后今日回升至{last}单,呈现反弹态势。'
+            elif recent[-1] < recent[-2]:
+                peak_text += f'近期呈回落趋势,需关注后续走势。'
+    items.append({'title': '峰值波动', 'content': peak_text})
+
+    # Paragraph 3: Activity / update
+    updated = metrics.get('updated_orders', 0)
+    prev_updated = metrics.get('prev_updated_orders', 0)
+    if updated > 0 or prev_updated > 0:
+        act_text = f'今日进度更新{updated}单'
+        if prev_updated > 0:
+            udiff = updated - prev_updated
+            upct = _pct_val(updated, prev_updated)
+            act_text += f',较昨日{"增加" if udiff >= 0 else "减少"}{abs(udiff)}单({upct:+.1f}%)'
+            if abs(upct) > 20:
+                act_text += ',团队活跃度波动较大。' if abs(upct) > 30 else ',团队活跃度有所变化。'
+            else:
+                act_text += ',团队活跃度保持平稳。'
+        else:
+            act_text += '。'
+        items.append({'title': '活跃度分析', 'content': act_text})
+
+    return items
+
+
+def _insight_status_structured(status_dist, prev_status_dist=None):
+    """Generate structured status distribution insight."""
+    items = []
+    total = sum(status_dist.values())
+    if not total:
+        items.append({'title': '状态概览', 'content': '暂无订单状态数据。'})
+        return items
+
+    max_status = max(status_dist.items(), key=lambda x: x[1])
+    max_pct = max_status[1] / total * 100
+
+    # Production share (C+D)
+    prod = status_dist.get('已付订金待生产', 0) + status_dist.get('已生产待付尾款', 0)
+    prod_pct = prod / total * 100
+
+    status_text = f'{max_status[0]}占比最高({max_status[1]}单,{max_pct:.1f}%)'
+    if prod_pct > 0:
+        status_text += f'。生产端(已付订金+已生产)合计{prod}单({prod_pct:.1f}%)'
+        if prod_pct > 30:
+            status_text += ',生产推进力度加大。'
+        else:
+            status_text += '。'
+    items.append({'title': '状态分布特征', 'content': status_text})
+
+    # WoW change
+    if prev_status_dist:
+        changes = []
+        for name, curr in status_dist.items():
+            prev = prev_status_dist.get(name, 0)
+            if prev > 0:
+                chg = _pct_val(curr, prev)
+                if abs(chg) > 5:
+                    changes.append(f'{name}{chg:+.1f}%')
+        if changes:
+            items.append({'title': '状态变化(vs 昨日)', 'content': ' | '.join(changes[:4])})
+
+    return items
+
+
+def _insight_region_structured(region_dist):
+    """Generate structured regional insight with top countries."""
+    items = []
+    if not region_dist:
+        items.append({'title': '区域概览', 'content': '暂无区域分布数据。'})
+        return items
+
+    sorted_regions = sorted(region_dist.items(), key=lambda x: -x[1]['qty'])
+    top3 = sorted_regions[:3]
+    total = sum(v['qty'] for v in region_dist.values())
+    top3_pct = sum(v['qty'] for _, v in top3) / total * 100 if total else 0
+
+    top_names = [k for k, _ in top3]
+    items.append({'title': '核心市场', 'content': f'{"、".join(top_names)}三大核心市场合计占比{top3_pct:.1f}%,是海外订单的核心增长引擎。'})
+
+    # Each region detail
+    for name, data in sorted_regions[:5]:
+        top_c = data.get('top_countries', [])
+        top_c_str = '/'.join([c['country'] for c in top_c[:3]]) if top_c else ''
+        change = data.get('change_pct', 0)
+        if change is None:
+            chg_str = ''
+        elif change > 0:
+            chg_str = f'(+{change:.1f}%)'
+        elif change < 0:
+            chg_str = f'({change:.1f}%)'
+        else:
+            chg_str = ''
+        content = f'{data["pct"]:.1f}% | {data["qty"]:,}台'
+        if top_c_str:
+            content += f' | {top_c_str}为主力'
+        if change != 0:
+            content += f' {chg_str}'
+        if change < 0:
+            content += ' | 需关注'
+        items.append({'title': name, 'content': content})
+
+    return items
+
+
+def _insight_team_structured(team, total_qty=0, per_capita=0, countries_covered=0):
+    """Generate structured team performance insight."""
+    items = []
+    if not team:
+        items.append({'title': '团队概览', 'content': '暂无团队绩效数据。'})
+        return items
+
+    n_members = len(team)
+    avg = per_capita or _safe_div(sum(v.get('orders', 0) for v in team.values()), n_members)
+    top = max(team.items(), key=lambda x: x[1].get('orders', 0))
+
+    overview = f'团队共{n_members}人,'
+    if countries_covered:
+        overview += f'覆盖{countries_covered}国,'
+    overview += f'人均追踪{avg:.0f}单。'
+    items.append({'title': '团队概况', 'content': overview})
+
+    # Top performers
+    sorted_team = sorted(team.items(), key=lambda x: -x[1].get('orders', 0))
+    for name, data in sorted_team[:2]:
+        orders = data.get('orders', 0)
+        qty = data.get('qty', 0)
+        comment = '增长主力' if orders > avg * 1.3 else '稳健跟进'
+        content = f'{name} {orders}单'
+        if qty:
+            content += f'/{qty:,}台'
+        content += f' - {comment}'
+        items.append({'title': '领跑者点评', 'content': content})
+
+    # Find laggard
+    low = min(team.items(), key=lambda x: x[1].get('orders', 0))
+    if low[1].get('orders', 0) < avg * 0.7:
+        items.append({'title': '关注提醒', 'content': f'{low[0]}订单量低于团队均值,建议关注产能提升空间。'})
+
+    return items
+
+
+def _insight_stage_structured(stage_analysis, funnel):
+    """Generate structured monthly stage funnel insight."""
+    items = []
+    early = stage_analysis.get('early', {})
+    mid = stage_analysis.get('mid', {})
+    late = stage_analysis.get('late', {})
+
+    a_b = early.get('orders', 0)
+    items.append({
+        'title': '前期Pipeline充足',
+        'content': f'合同拟定中(A) + 已锁定待付订金(B)共{a_b}单,占总量的{early.get("pct", 0):.1f}%,后续转化空间充足。'
+    })
+
+    c_d = mid.get('orders', 0)
+    items.append({
+        'title': '中期生产推进',
+        'content': f'已付订金待生产(C) + 已生产待付尾款(D)共{c_d}单,占总量的{mid.get("pct", 0):.1f}%。'
+    })
+
+    e_f = late.get('orders', 0)
+    items.append({
+        'title': '后期交付待加速',
+        'content': f'已付尾款待发运(E) + 已发运(F)仅{e_f}单,占总量的{late.get("pct", 0):.1f}%。'
+    })
+
+    if early.get('pct', 0) > 40:
+        items.append({
+            'title': '⚠ 风险提示',
+            'content': f'近半数订单仍停留在合同拟定阶段,需关注A→B的转化效率,加速合同确认和订金回收。'
+        })
+
+    return items
+
+
+def _insight_top_countries_structured(top_countries_change, total_qty, top_n=6):
+    """Generate structured TOP countries insight."""
+    items = []
+    if not top_countries_change:
+        items.append({'title': '国家概览', 'content': '暂无国家分布数据。'})
+        return items
+
+    sorted_items = sorted(top_countries_change.items(), key=lambda x: -x[1]['qty'])
+    top_list = sorted_items[:top_n]
+    total_top = sum(v['qty'] for _, v in top_list)
+    pct = total_top / total_qty * 100 if total_qty else 0
+
+    items.append({'title': '集中度分析', 'content': f'Top {top_n}目的国合计覆盖{total_top:,}台,占总量的{pct:.1f}%,重点市场集中度高。'})
+
+    for i, (country, data) in enumerate(top_list, 1):
+        chg = data.get('change_pct', 0)
+        comment = ''
+        if chg is None:
+            comment = '新增市场,潜力可期'
+            chg_str = ''
+        elif chg > 30:
+            comment = '本周增长最快市场之一'
+            chg_str = f'({chg:+.1f}%)'
+        elif chg > 10:
+            comment = '持续增长'
+            chg_str = f'({chg:+.1f}%)'
+        elif chg < -10:
+            comment = '虽有下滑但仍高位' if data['qty'] > total_qty * 0.05 else '需关注'
+            chg_str = f'({chg:.1f}%)'
+        elif chg < 0:
+            comment = '小幅回落'
+            chg_str = f'({chg:.1f}%)'
+        else:
+            comment = 'steady增长' if i <= 3 else '新兴市场,潜力可期'
+            chg_str = f'({chg:+.1f}%)' if chg != 0 else ''
+        items.append({'title': f'{i}. {country} {data["qty"]:,}台{chg_str}', 'content': comment})
+
+    return items
+
+
+# ==============================================================================
+# TEXT / LAYOUT HELPERS
+# ==============================================================================
+
+def _truncate_text(text, max_chars=60):
+    """Truncate text to max_chars, appending '...' if truncated."""
+    if not text:
+        return text
+    if len(text) > max_chars:
+        return text[:max_chars - 1] + '...'
+    return text
+
+
+def _sentiment_color(text):
+    """Return a light background color based on text sentiment."""
+    if not text:
+        return None
+    text = str(text)
+    positive_words = ['提升', '增长', '上调', '增加', '高', '好', '大幅', '冲刺', '领跑', '上升', '扩大', '优化', '改善', '突破', '达成']
+    negative_words = ['下滑', '下降', '减少', '低', '差', '回落', '下滑', '滞后', '堆积', '阻塞', '缺口', '延迟', '超期', '逾期', '风险', '警告']
+    pos_score = sum(1 for w in positive_words if w in text)
+    neg_score = sum(1 for w in negative_words if w in text)
+    if neg_score > pos_score:
+        return RGBColor(0xFE, 0xE2, 0xE2)  # light red ~ #EF444420
+    if pos_score > neg_score:
+        return RGBColor(0xD1, 0xFA, 0xE5)  # light green ~ #10B98120
+    return None
+
+
+import re
+
+def _emoji_for_item(title):
+    """Return an emoji prefix based on title keywords."""
+    if not title:
+        return '📈'
+    title = str(title)
+    # Skip if title already starts with an emoji
+    if re.match(r'^[\U0001F300-\U0001F9FF\u2600-\u26FF\u2700-\u27BF]', title):
+        return ''
+    if any(k in title for k in ['风险', '警告', '关注', '下滑', '下降', '延迟', '超期', '缺口', '阻塞']):
+        return '⚠️'
+    if any(k in title for k in ['建议', '措施', '行动', '协调', '对接']):
+        return '💡'
+    if any(k in title for k in ['目标', '计划', '冲刺', '展望', '聚焦']):
+        return '🎯'
+    if any(k in title for k in ['增长', '上升', '提升', '峰值', '领跑', '突破', '活跃', '好转']):
+        return '📈'
+    return '💡'
+
+
+def _add_footer_if_missing(slide, footer_text, slide_width=None):
+    """Add a bottom footer bar to slides that don't already have one (Cover, TOC, End)."""
+    if slide_width is None:
+        slide_width = slide.shapes._spTree.getparent().getparent().attrib.get('cx')
+        slide_width = Emu(int(slide_width)) if slide_width else Emu(16256000)
+    # Check if footer already exists
+    has_footer = False
+    for shape in slide.shapes:
+        if shape.has_text_frame and '数据来源' in shape.text_frame.text:
+            has_footer = True
+            break
+    if has_footer:
+        return
+    bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, Emu(8824000), slide_width, Emu(320000))
+    bar.fill.solid()
+    bar.fill.fore_color.rgb = C_PRIMARY
+    bar.line.fill.background()
+    box = slide.shapes.add_textbox(Emu(762000), Emu(8824000), Emu(14000000), Emu(320000))
+    p = box.text_frame.paragraphs[0]
+    p.text = footer_text
+    p.font.size = Pt(10)
+    p.font.color.rgb = C_WHITE
+    p.font.name = '微软雅黑'
+
+
+def _ensure_word_wrap_all(slide):
+    """Enable word_wrap on all text frames in a slide."""
+    for shape in slide.shapes:
+        if shape.has_text_frame:
+            shape.text_frame.word_wrap = True
+            for para in shape.text_frame.paragraphs:
+                for run in para.runs:
+                    run.font.name = '微软雅黑'
+
+
+# ==============================================================================
+# MATH HELPERS
+# ==============================================================================
+
+def _pct_val(curr, prev):
+    if prev and prev != 0:
+        return (curr - prev) / prev * 100
+    return None
+
+def _format_pct(pct, with_sign=True, suffix='%', zero_suffix=''):
+    """Safely format a percentage value. Returns '—' if pct is None."""
+    if pct is None:
+        return '—'
+    sign = '+' if with_sign and pct >= 0 else ''
+    return f"{sign}{pct:.1f}{suffix}{zero_suffix}"
+
+
+def _pct_str(curr, prev):
+    if prev and prev != 0:
+        pct = round((curr - prev) / prev * 100, 1)
+        sign = '+' if pct >= 0 else ''
+        return f"{sign}{pct}% vs 上期"
+    return "—"
+
+
+def _safe_div(a, b):
+    return round(a / b, 1) if b else 0
+
+
+
+# ==============================================================================
+# DAILY REPORT
+# ==============================================================================
+
+def build_daily_report(data_file: str, date: datetime, output_path: str,
+                       department='海外事业部', source='海外订单日报系统'):
+    master_path = get_master_template('daily')
+    prs = Presentation(master_path)
+    content_top = _detect_content_top(prs.slides[1])
+
+    df = load_daily(data_file, date)
+    prev_date = date - timedelta(days=1)
+    try:
+        prev_df = load_daily(data_file, prev_date)
+    except Exception:
+        prev_df = None
+
+    metrics = calc_daily_metrics(df, prev_df)
+    prev_metrics = calc_daily_metrics(prev_df, None) if prev_df is not None else {}
+    date_str = date.strftime('%Y年%m月%d日')
+    period_str = date.strftime('%Y年%m月%d日')
+
+    # ---- Page 1: Cover ----
+    slide = _duplicate_slide(prs, prs.slides[0])
+    _replace_all_placeholders(slide, {
+        '{report_title}': '海外订单数据日报',
+        '{report_type}': '数据日报',
+        '{date}': date_str,
+        '{department}': department,
+        '{period}': period_str,
+        '{gen_time}': datetime.now().strftime('%Y-%m-%d %H:%M'),
+    })
+    _add_footer_if_missing(slide, f'数据来源:{source} | 1/8')
+    cover_kpis = [
+        ('在跟订单', metrics['tracking_orders'], '单',
+         _pct_str(metrics['tracking_orders'], metrics.get('prev_tracking_orders', 0))),
+        ('订单总数量', f"{metrics['total_qty']:,}", '台',
+         _pct_str(metrics['total_qty'], metrics.get('prev_total_qty', 0))),
+        ('今日已更新', metrics['updated_orders'], '单',
+         _pct_str(metrics['updated_orders'], metrics.get('prev_updated_orders', 0))),
+        ('支持需求', metrics['support_requests'], '项', '需跨部门协调'),
+    ]
+    for i, (lbl, val, unit, chg) in enumerate(cover_kpis, 1):
+        _replace_placeholder(slide, f'{{kpi{i}_label}}', lbl)
+        _replace_placeholder(slide, f'{{kpi{i}_value}}', str(val))
+
+    # ---- Page 2: KPI Overview ----
+    s2 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s2, {
+        '{report_title}': '海外订单数据日报',
+        '{date}': date_str,
+        '{page_title}': '今日核心指标概览',
+        '{source}': source,
+        '{period}': '2/8',
+        '{page_num}': '',
+    })
+    kpis = [
+        {'label': '在跟订单总数', 'value': metrics['tracking_orders'], 'unit': '单',
+         'change': _pct_str(metrics['tracking_orders'], metrics.get('prev_tracking_orders', 0)),
+         'sub': '日均跟踪'},
+        {'label': '订单总数量', 'value': f"{metrics['total_qty']:,}", 'unit': '台',
+         'change': _pct_str(metrics['total_qty'], metrics.get('prev_total_qty', 0)),
+         'sub': '规模稳定'},
+        {'label': '今日已更新', 'value': metrics['updated_orders'], 'unit': '单',
+         'change': _pct_str(metrics['updated_orders'], metrics.get('prev_updated_orders', 0)),
+         'sub': '团队活跃'},
+        {'label': '下月预测交付', 'value': metrics['forecast_next'], 'unit': '台',
+         'change': _pct_str(metrics['forecast_next'], metrics.get('prev_forecast_next', 0)),
+         'sub': '交付预期'},
+        {'label': '支持需求总数', 'value': metrics['support_requests'], 'unit': '项',
+         'change': '需跨部门协调', 'sub': '建议集中处理'},
+        {'label': '已发运订单', 'value': metrics['shipped_orders'], 'unit': '单',
+         'change': f'共{metrics.get("shipped_orders", 0) * 8}台 | {metrics["shipped_orders"] - metrics.get("prev_shipped_orders", 0)}单 vs 昨日',
+         'sub': '交付稳步推进'},
+    ]
+    _add_kpi_cards(s2, kpis, start_y=Emu(content_top))
+
+    # ---- Page 3: 10-Day Trend ----
+    s3 = _duplicate_slide(prs, prs.slides[1])
+    trend_dates = []
+    trend_vals = []
+    for d_offset in range(-9, 1):
+        d = date + timedelta(days=d_offset)
+        try:
+            tdf = load_daily(data_file, d)
+            trend_dates.append(d.strftime('%m/%d'))
+            trend_vals.append(len(tdf))
+        except Exception:
+            pass
+
+    # Generate conclusion title
+    trend_conclusion = '近10天订单趋势'
+    if len(trend_vals) >= 2:
+        if trend_vals[-1] > trend_vals[-2]:
+            trend_conclusion = '近10天订单趋势:订单规模回升'
+        elif trend_vals[-1] < trend_vals[-2]:
+            trend_conclusion = '近10天订单趋势:订单量继续回落'
+        peak = max(trend_vals)
+        if trend_vals[-1] == peak:
+            trend_conclusion = '近10天订单趋势:今日达到峰值'
+
+    _replace_all_placeholders(s3, {
+        '{report_title}': '海外订单数据日报',
+        '{date}': date_str,
+        '{page_title}': trend_conclusion,
+        '{source}': source,
+        '{period}': '3/8',
+        '{page_num}': '',
+    })
+    if len(trend_dates) >= 2:
+        add_column_chart(s3, trend_dates, trend_vals,
+                         Emu(762000), Emu(content_top), Emu(8890000), Emu(5334000),
+                         series_name='订单量', color=C_ACCENT,
+                         category_axis_title='日期', value_axis_title='订单数')
+        insight_items = generate_deep_insights('daily', 'trend', metrics,
+                                                prev_metrics=prev_metrics,
+                                                trend_dates=trend_dates,
+                                                trend_vals=trend_vals)
+        _add_structured_insight(s3, insight_items,
+                                Emu(9906000), Emu(content_top), Emu(4826000), Emu(5334000))
+
+    # ---- Page 4: Status Distribution ----
+    s4 = _duplicate_slide(prs, prs.slides[1])
+    status_names = list(metrics['status_dist'].keys())
+    status_vals = list(metrics['status_dist'].values())
+    total_status = sum(status_vals)
+    prod_share = metrics.get('production_share', 0)
+    status_title = '订单状态分布'
+    if prod_share > 0:
+        status_title = f'订单状态分布:生产端占比提升至{prod_share:.1f}%'
+
+    _replace_all_placeholders(s4, {
+        '{report_title}': '海外订单数据日报',
+        '{date}': date_str,
+        '{page_title}': status_title,
+        '{source}': source,
+        '{period}': '4/8',
+        '{page_num}': '',
+    })
+    if status_names and total_status > 0:
+        # Left donut chart: 55% width
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        add_doughnut_chart(s4, status_names, status_vals,
+                           Emu(762000), Emu(content_top), chart_w, Emu(5334000),
+                           show_legend=True, show_data_labels=True, show_percent=True,
+                           ring_ratio=0.6)
+        # Right side: status change + deep insights (no table to save space for dense text)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+
+        prev_status = prev_metrics.get('status_dist', {})
+        # Status change text: "合同拟定中 -30.8% ↓ | 已付订金待生产 +55.6% ↑"
+        changes = []
+        for name, curr in metrics['status_dist'].items():
+            prev = prev_status.get(name, 0)
+            if prev > 0:
+                chg = _pct_val(curr, prev)
+                if chg is not None:
+                    arrow = '↑' if chg >= 0 else '↓'
+                    changes.append(f'{name} {_format_pct(chg)}{arrow}')
+        if changes:
+            change_text = ' | '.join(changes[:4])
+            _add_text_block(s4, '状态变化(vs 昨日)', change_text,
+                            text_left, Emu(content_top), text_w, Emu(609600),
+                            title_size=Pt(12), body_size=Pt(10))
+
+        # Deep insight fills remaining right-side space
+        insight_items = generate_deep_insights('daily', 'status', metrics,
+                                               prev_status_dist=prev_status)
+        insight_top = Emu(int(content_top) + 685800)
+        insight_height = Emu(5334000 - 685800)
+        _add_structured_insight(s4, insight_items,
+                                text_left, insight_top, text_w, insight_height)
+
+    # ---- Page 5: Owner Distribution ----
+    s5 = _duplicate_slide(prs, prs.slides[1])
+    owner_names = list(metrics['owner_dist'].keys())[:8]
+    owner_vals = list(metrics['owner_dist'].values())[:8]
+    top_owner = owner_names[0] if owner_names else ''
+    second_owner = owner_names[1] if len(owner_names) > 1 else ''
+    owner_title = '负责人订单分布'
+    if top_owner and second_owner:
+        owner_title = f'负责人订单分布:{top_owner}、{second_owner}领跑'
+
+    _replace_all_placeholders(s5, {
+        '{report_title}': '海外订单数据日报',
+        '{date}': date_str,
+        '{page_title}': owner_title,
+        '{source}': source,
+        '{period}': '5/8',
+        '{page_num}': '',
+    })
+    if owner_names:
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+        add_horizontal_bar_chart(s5, owner_names, owner_vals,
+                                 Emu(762000), Emu(content_top), chart_w, Emu(5334000),
+                                 series_name='订单笔数', color=C_ACCENT, reverse_order=True,
+                                 value_axis_title='订单笔数')
+        # Deep insight: owner distribution analysis
+        prev_owner_dist = prev_metrics.get('owner_dist', {}) if prev_metrics else {}
+        insight_items = generate_deep_insights('daily', 'owner', metrics,
+                                               prev_owner_dist=prev_owner_dist)
+        _add_structured_insight(s5, insight_items,
+                                text_left, Emu(content_top), text_w, Emu(5334000))
+
+    # ---- Page 6: Country TOP8 ----
+    s6 = _duplicate_slide(prs, prs.slides[1])
+    countries = list(metrics['country_top8'].keys())[:8]
+    country_vals = list(metrics['country_top8'].values())[:8]
+    top_country = countries[0] if countries else ''
+    country_title = '目的国家TOP8'
+    if top_country:
+        country_title = f'目的国家TOP8:{top_country}订单量领先'
+
+    _replace_all_placeholders(s6, {
+        '{report_title}': '海外订单数据日报',
+        '{date}': date_str,
+        '{page_title}': country_title,
+        '{source}': source,
+        '{period}': '6/8',
+        '{page_num}': '',
+    })
+    if countries:
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+        add_horizontal_bar_chart(s6, countries, country_vals,
+                                 Emu(762000), Emu(content_top), chart_w, Emu(5334000),
+                                 series_name='订单量(台)', color=C_ACCENT, reverse_order=True,
+                                 value_axis_title='订单量(台)')
+        # Deep insight: country top8 analysis
+        insight_items = generate_deep_insights('daily', 'country', metrics)
+        _add_structured_insight(s6, insight_items,
+                                text_left, Emu(content_top), text_w, Emu(5334000))
+
+    # ---- Page 7: Support Analysis ----
+    s7 = _duplicate_slide(prs, prs.slides[1])
+    alerts = metrics['alerts']
+    alert_title = '异常告警'
+    if alerts:
+        severe = [a for a in alerts if a.get('level') == '严重']
+        if severe:
+            alert_title = f'异常告警:{severe[0]["title"]}'
+        else:
+            alert_title = f'异常告警:{alerts[0]["title"]}'
+
+    _replace_all_placeholders(s7, {
+        '{report_title}': '海外订单数据日报',
+        '{date}': date_str,
+        '{page_title}': alert_title,
+        '{source}': source,
+        '{period}': '7/8',
+        '{page_num}': '',
+    })
+
+    # Unified left-chart + right-insight layout
+    sc = metrics.get('support_categories', {})
+    chart_w = Emu(int(prs.slide_width) * 0.55)
+    text_left = Emu(int(prs.slide_width) * 0.62)
+    text_w = Emu(int(prs.slide_width) * 0.36)
+
+    if sc:
+        sc_names = list(sc.keys())
+        sc_vals = list(sc.values())
+        add_doughnut_chart(s7, sc_names, sc_vals,
+                           Emu(762000), Emu(content_top), chart_w, Emu(5334000),
+                           show_legend=True, show_data_labels=True, show_percent=True,
+                           ring_ratio=0.6)
+
+    # Deep insight: alerts & support analysis
+    insight_items = generate_deep_insights('daily', 'alert', metrics)
+    _add_structured_insight(s7, insight_items,
+                            text_left, Emu(content_top), text_w, Emu(5334000))
+
+    # ---- Page 8: Key Points ----
+    s8 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s8, {
+        '{report_title}': '海外订单数据日报',
+        '{date}': date_str,
+        '{page_title}': '明日工作重点',
+        '{source}': source,
+        '{period}': '8/8',
+        '{page_num}': '',
+    })
+
+    # Left chart: overdue orders horizontal bar (or support categories fallback)
+    overdue = metrics.get('overdue_orders', [])
+    chart_w = Emu(int(prs.slide_width) * 0.55)
+    text_left = Emu(int(prs.slide_width) * 0.62)
+    text_w = Emu(int(prs.slide_width) * 0.36)
+
+    if overdue:
+        o_countries = [o['country'] for o in overdue[:8]]
+        o_days = [o['days'] for o in overdue[:8]]
+        add_horizontal_bar_chart(s8, o_countries, o_days,
+                                 Emu(762000), Emu(content_top), chart_w, Emu(4826000),
+                                 series_name='超期天数', color=C_ORANGE, reverse_order=True,
+                                 value_axis_title='天数')
+    elif metrics.get('support_categories'):
+        sc = metrics['support_categories']
+        add_horizontal_bar_chart(s8, list(sc.keys()), list(sc.values()),
+                                 Emu(762000), Emu(content_top), chart_w, Emu(4826000),
+                                 series_name='需求数', color=C_ACCENT, reverse_order=True,
+                                 value_axis_title='数量')
+
+    # Deep insight: tomorrow's action items
+    action_items = generate_deep_insights('daily', 'action', metrics)
+    _add_structured_insight(s8, action_items,
+                            text_left, Emu(content_top), text_w, Emu(5334000))
+
+    for slide in prs.slides:
+        _ensure_word_wrap_all(slide)
+    _delete_template_slides(prs)
+    prs.save(output_path)
+    print(f"Daily report saved: {output_path}")
+
+
+
+
+
+# ==============================================================================
+# WEEKLY REPORT
+# ==============================================================================
+
+def build_weekly_report(data_file: str, year: int, week: int, output_path: str,
+                        department='海外事业部', source='海外订单日报系统'):
+    master_path = get_master_template('weekly')
+    prs = Presentation(master_path)
+    content_top = _detect_content_top(prs.slides[1])
+
+    df, prev_df = load_weekly(data_file, year, week)
+    metrics = calc_weekly_metrics(df, prev_df)
+
+    period_str = f"{year}年第{week}周"
+    date_range_str = f"{df['_data_date'].min().strftime('%m/%d')} - {df['_data_date'].max().strftime('%m/%d')}"
+
+    # Page 1: Cover
+    slide = _duplicate_slide(prs, prs.slides[0])
+    _replace_all_placeholders(slide, {
+        '{report_title}': '海外订单数据周报',
+        '{report_type}': 'Weekly Overseas Order Data Report',
+        '{date}': period_str,
+        '{department}': department,
+        '{period}': date_range_str,
+        '{gen_time}': datetime.now().strftime('%Y-%m-%d %H:%M'),
+    })
+    _add_footer_if_missing(slide, f'数据来源:{source} | 1/9')
+    # Fix {kpi4_label} {kpi4_value} with actual metric (下月预测交付)
+    cover_kpis = [
+        ('跟踪订单笔数', f"{metrics['tracking_orders']:,}", '笔',
+         _pct_str(metrics['tracking_orders'], metrics.get('prev_tracking_orders', 0))),
+        ('订单总数量', f"{metrics['total_qty']:,}", '台',
+         _pct_str(metrics['total_qty'], metrics.get('prev_total_qty', 0))),
+        ('覆盖目的国', f"{metrics['countries']}", '个', '全球布局持续深化'),
+        ('下月预测交付', f"{metrics['forecast_next']:,}", '台',
+         _pct_str(metrics['forecast_next'], metrics.get('prev_forecast_next', 0))),
+    ]
+    for i, (lbl, val, unit, chg) in enumerate(cover_kpis, 1):
+        _replace_placeholder(slide, f'{{kpi{i}_label}}', lbl)
+        _replace_placeholder(slide, f'{{kpi{i}_value}}', str(val))
+
+    nav_labels = ['周汇总', '趋势图', '环比分析', '区域排行', '问题建议', '下周计划']
+
+    # Page 2: Weekly Summary (KPI cards)
+    s2 = _duplicate_slide(prs, prs.slides[1])
+    t_chg = _pct_val(metrics['tracking_orders'], metrics.get('prev_tracking_orders', 0))
+    q_chg = _pct_val(metrics['total_qty'], metrics.get('prev_total_qty', 0))
+    t_chg_str = _format_pct(t_chg)
+    q_chg_str = _format_pct(q_chg)
+    _replace_all_placeholders(s2, {
+        '{report_title}': '海外订单数据周报',
+        '{date}': period_str,
+        '{page_title}': f"周汇总:跟踪订单环比{t_chg_str},订单总量增长{q_chg_str}",
+        '{source}': source,
+        '{period}': '2/9',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s2, nav_labels, active_index=0, slide_width=prs.slide_width)
+    kpis = [
+        {'label': '跟踪订单笔数', 'value': f"{metrics['tracking_orders']:,}", 'unit': '笔',
+         'change': f'{t_chg_str} vs W1 ({metrics.get("prev_tracking_orders", 0)}笔)' if t_chg is not None else ('新增' if metrics['tracking_orders'] > 0 else '—'),
+         'sub': f'日均{metrics["avg_daily_orders"]:.0f}笔'},
+        {'label': '订单总数量', 'value': f"{metrics['total_qty']:,}", 'unit': '台',
+         'change': f'{q_chg_str} vs W1 ({metrics.get("prev_total_qty", 0):,})' if q_chg is not None else ('新增' if metrics['total_qty'] > 0 else '—'),
+         'sub': f'日均{metrics["avg_daily_qty"]:.0f}台'},
+        {'label': '已发运订单', 'value': metrics['shipped_orders'], 'unit': '笔',
+         'change': _pct_str(metrics['shipped_orders'], metrics.get('prev_shipped_orders', 0)),
+         'sub': '环比大幅提升'},
+        {'label': '覆盖目的国', 'value': metrics['countries'], 'unit': '个',
+         'change': '持平', 'sub': '全球布局持续深化'},
+        {'label': '进度更新订单', 'value': metrics['updated_orders'], 'unit': '笔',
+         'change': _pct_str(metrics['updated_orders'], metrics.get('prev_updated_orders', 0)),
+         'sub': '推进效率提升'},
+        {'label': '下月预测交付', 'value': f"{metrics['forecast_next']:,}", 'unit': '台',
+         'change': _pct_str(metrics['forecast_next'], metrics.get('prev_forecast_next', 0)),
+         'sub': '交付预期大幅上调'},
+    ]
+    _add_kpi_cards(s2, kpis, start_y=Emu(content_top))
+
+    # Page 3: 7-Day Trend
+    s3 = _duplicate_slide(prs, prs.slides[1])
+    trend = metrics.get('daily_trend', {})
+    trend_title = '7日趋势:订单总量稳步上升'
+    if trend:
+        dates = list(trend.keys())
+        vals = list(trend.values())
+        if vals:
+            peak = max(vals)
+            peak_date = dates[vals.index(peak)]
+            if vals[-1] >= vals[0]:
+                trend_title = f'7日趋势:订单总量稳步上升,峰值出现在{peak_date}'
+            else:
+                trend_title = f'7日趋势:订单量有所波动,峰值出现在{peak_date}'
+    _replace_all_placeholders(s3, {
+        '{report_title}': '海外订单数据周报',
+        '{date}': period_str,
+        '{page_title}': trend_title,
+        '{source}': source,
+        '{period}': '3/9',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s3, nav_labels, active_index=1, slide_width=prs.slide_width)
+    trend = metrics.get('daily_trend', {})
+    if trend:
+        dates = list(trend.keys())
+        vals = list(trend.values())
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+        add_line_chart(s3, dates, vals,
+                       Emu(762000), Emu(content_top), chart_w, Emu(5334000),
+                       series_name='订单量', color=C_ACCENT,
+                       category_axis_title='日期(MM/DD)', value_axis_title='订单数')
+        peak = max(vals) if vals else 0
+        peak_date = dates[vals.index(peak)] if vals else ''
+        avg = sum(vals) // len(vals) if vals else 0
+        prev_avg = metrics.get('prev_avg_daily_orders', 0)
+        above_days = metrics.get('days_above_prev_avg', 0)
+        total_days = len(vals)
+
+        # Deep insight: weekly trend analysis
+        insight_items = generate_deep_insights('weekly', 'weekly_trend', metrics,
+                                               trend_dates=dates, trend_vals=vals)
+        _add_structured_insight(s3, insight_items,
+                                text_left, Emu(content_top), text_w, Emu(5334000))
+
+    # Page 4: WoW Analysis
+    s4 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s4, {
+        '{report_title}': '海外订单数据周报',
+        '{date}': period_str,
+        '{page_title}': '环比分析:各阶段全面增长,已发运环节增幅最大',
+        '{source}': source,
+        '{period}': '4/9',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s4, nav_labels, active_index=2, slide_width=prs.slide_width)
+    sw_data = metrics.get('status_wow', {})
+    if sw_data:
+        names = list(sw_data.keys())
+        current_vals = [v['current'] for v in sw_data.values()]
+        previous_vals = [v['previous'] for v in sw_data.values()]
+        # Replace None with 0 for chart data to avoid crashes
+        changes = [v['change_pct'] if v['change_pct'] is not None else 0 for v in sw_data.values()]
+        
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+        
+        # Grouped bar chart: 本期 vs 上期
+        add_grouped_bar_chart(s4, names,
+                              [('本期', current_vals), ('上期', previous_vals)],
+                              Emu(762000), Emu(content_top), chart_w, Emu(4826000),
+                              colors=[C_ACCENT, C_SECONDARY],
+                              show_legend=True, show_data_labels=True,
+                              category_axis_title='订单状态', value_axis_title='订单数')
+        
+        # WoW增幅 labels below chart
+        wow_label_items = []
+        for k, v in sw_data.items():
+            pct_str = _format_pct(v['change_pct'])
+            arrow = '↑' if v['change_pct'] is not None and v['change_pct'] >= 0 else '↓' if v['change_pct'] is not None else ''
+            wow_label_items.append(f'{k} {pct_str}{arrow}')
+        _add_text_block(s4, '环比增幅', ' | '.join(wow_label_items),
+                        Emu(762000), Emu(6604000), chart_w, Emu(609600),
+                        title_size=Pt(11), body_size=Pt(10))
+
+        # Deep insight: WoW analysis
+        insight_items = generate_deep_insights('weekly', 'weekly_wow', metrics)
+        _add_structured_insight(s4, insight_items,
+                                text_left, Emu(content_top), text_w, Emu(4826000))
+
+    # Page 5: Region Distribution
+    s5 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s5, {
+        '{report_title}': '海外订单数据周报',
+        '{date}': period_str,
+        '{page_title}': '区域分布:中东增速领跑,欧洲为唯一下滑区域',
+        '{source}': source,
+        '{period}': '5/9',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s5, nav_labels, active_index=3, slide_width=prs.slide_width)
+    rdist = metrics.get('region_dist', {})
+    if rdist:
+        regions = list(rdist.keys())
+        qtys = [v['qty'] for v in rdist.values()]
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+        add_doughnut_chart(s5, regions, qtys,
+                           Emu(762000), Emu(content_top), chart_w, Emu(5334000),
+                           show_legend=True, show_data_labels=True, show_percent=True,
+                           ring_ratio=0.6)
+        # Deep insight: regional analysis
+        insight_items = generate_deep_insights('weekly', 'weekly_region', metrics)
+        _add_structured_insight(s5, insight_items,
+                                text_left, Emu(content_top), text_w, Emu(5334000))
+
+    # Page 6: TOP Countries
+    s6 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s6, {
+        '{report_title}': '海外订单数据周报',
+        '{date}': period_str,
+        '{page_title}': 'TOP国家排行:科威特344台居首,TOP15贡献70%+总量',
+        '{source}': source,
+        '{period}': '6/9',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s6, nav_labels, active_index=3, slide_width=prs.slide_width)
+    topc_change = metrics.get('top_countries_change', {})
+    if topc_change:
+        countries = list(topc_change.keys())[:10]
+        vals = [v['qty'] for v in list(topc_change.values())[:10]]
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+        add_horizontal_bar_chart(s6, countries, vals,
+                                 Emu(762000), Emu(content_top), chart_w, Emu(4826000),
+                                 series_name='订单量(台)', color=C_ACCENT, reverse_order=True,
+                                 value_axis_title='订单量(台)')
+        # Deep insight: top countries analysis
+        insight_items = generate_deep_insights('weekly', 'weekly_country', metrics)
+        _add_structured_insight(s6, insight_items,
+                                text_left, Emu(content_top), text_w, Emu(4826000))
+
+    # Page 7: Team Tracking
+    s7 = _duplicate_slide(prs, prs.slides[1])
+    team = metrics.get('team', {})
+    n_members = len(team.get('owners', {})) if team else 0
+    n_growers = sum(1 for v in metrics.get('team_wow', {}).values() if v.get('change', 0) > 0)
+    team_title = f'团队追踪:{n_members}人团队全面覆盖,{n_growers}人实现增长'
+    _replace_all_placeholders(s7, {
+        '{report_title}': '海外订单数据周报',
+        '{date}': period_str,
+        '{page_title}': team_title,
+        '{source}': source,
+        '{period}': '7/9',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s7, nav_labels, active_index=4, slide_width=prs.slide_width)
+    team = metrics.get('team', {})
+    owners = team.get('owners', {})
+    if owners:
+        names = list(owners.keys())
+        vals = list(owners.values())
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+        add_horizontal_bar_chart(s7, names, vals,
+                                 Emu(762000), Emu(content_top), chart_w, Emu(4826000),
+                                 series_name='订单笔数', color=C_ACCENT, reverse_order=True,
+                                 value_axis_title='订单笔数')
+        # Deep insight: team tracking
+        insight_items = generate_deep_insights('weekly', 'weekly_team', metrics)
+        _add_structured_insight(s7, insight_items,
+                                text_left, Emu(content_top), text_w, Emu(4826000))
+
+    # Page 8: Issues
+    s8 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s8, {
+        '{report_title}': '海外订单数据周报',
+        '{date}': period_str,
+        '{page_title}': '问题识别:系统数据匹配问题为本周首要障碍',
+        '{source}': source,
+        '{period}': '8/9',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s8, nav_labels, active_index=4, slide_width=prs.slide_width)
+    # Left chart: support request categories
+    sc = metrics.get('support_categories', {})
+    chart_w = Emu(int(prs.slide_width) * 0.55)
+    text_left = Emu(int(prs.slide_width) * 0.62)
+    text_w = Emu(int(prs.slide_width) * 0.36)
+    if sc:
+        add_horizontal_bar_chart(s8, list(sc.keys()), list(sc.values()),
+                                 Emu(762000), Emu(content_top), chart_w, Emu(4826000),
+                                 series_name='需求数', color=C_ORANGE, reverse_order=True,
+                                 value_axis_title='数量')
+    # Right: deep insight
+    insight_items = generate_deep_insights('weekly', 'weekly_issue', metrics)
+    _add_structured_insight(s8, insight_items,
+                            text_left, Emu(content_top), text_w, Emu(5334000))
+
+    # Page 9: Next Week Plan
+    s9 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s9, {
+        '{report_title}': '海外订单数据周报',
+        '{date}': period_str,
+        '{page_title}': '下周计划:聚焦发运交付,冲刺交付目标',
+        '{source}': source,
+        '{period}': '9/9',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s9, nav_labels, active_index=5, slide_width=prs.slide_width)
+
+    # Left chart: goals as column chart
+    goals = metrics.get('next_week_goals', [])
+    chart_w = Emu(int(prs.slide_width) * 0.55)
+    text_left = Emu(int(prs.slide_width) * 0.62)
+    text_w = Emu(int(prs.slide_width) * 0.36)
+    if goals:
+        goal_names = [g['title'].split(':')[0] for g in goals[:4]]
+        goal_nums = [g.get('number', 0) for g in goals[:4]]
+        add_column_chart(s9, goal_names, goal_nums,
+                         Emu(762000), Emu(content_top), chart_w, Emu(4826000),
+                         series_name='目标数量', color=C_ACCENT,
+                         category_axis_title='目标', value_axis_title='数量')
+
+    # Deep insight: next week plan
+    insight_items = generate_deep_insights('weekly', 'weekly_plan', metrics)
+    _add_structured_insight(s9, insight_items,
+                            text_left, Emu(content_top), text_w, Emu(5334000))
+
+    for slide in prs.slides:
+        _ensure_word_wrap_all(slide)
+    _delete_template_slides(prs)
+    prs.save(output_path)
+    print(f"Weekly report saved: {output_path}")
+
+
+
+
+
+
+# ==============================================================================
+# MONTHLY REPORT
+# ==============================================================================
+
+def build_monthly_report(data_file: str, year: int, month: int, output_path: str,
+                         department='海外事业部', source='海外订单日报系统'):
+    master_path = get_master_template('monthly')
+    prs = Presentation(master_path)
+    content_top = _detect_content_top(prs.slides[1])
+
+    df, prev_df, yoy_df = load_monthly(data_file, year, month)
+    metrics = calc_monthly_metrics(df, prev_df, yoy_df)
+
+    period_str = f"{year}年{month}月"
+
+    # Page 1: Cover
+    slide = _duplicate_slide(prs, prs.slides[0])
+    _replace_all_placeholders(slide, {
+        '{report_title}': '海外订单月度数据报告',
+        '{report_type}': 'Monthly Overseas Order Data Report',
+        '{date}': period_str,
+        '{department}': department,
+        '{period}': period_str,
+        '{gen_time}': datetime.now().strftime('%Y-%m-%d %H:%M'),
+    })
+    _add_footer_if_missing(slide, f'数据来源:{source} | 1/11')
+    cover_kpis = [
+        ('合同总数', f"{metrics['total_contracts']:,}"),
+        ('车辆总数', f"{metrics['total_qty']:,}"),
+        ('目的国家', f"{metrics['countries']}+"),
+        ('负责团队', '9人'),
+    ]
+    for i, (lbl, val) in enumerate(cover_kpis, 1):
+        _replace_placeholder(slide, f'{{kpi{i}_label}}', lbl)
+        _replace_placeholder(slide, f'{{kpi{i}_value}}', val)
+
+    # Page 2: TOC
+    s_toc = _duplicate_slide(prs, prs.slides[2])
+    _add_footer_if_missing(s_toc, f'数据来源:{source} | 2/11')
+    _replace_all_placeholders(s_toc, {
+        '{chapter1_title}': '月度总览',
+        '{chapter1_desc}': '核心KPI一览:合同总数、车辆规模、新签与发运表现',
+        '{chapter2_title}': '订单状态分析',
+        '{chapter2_desc}': '订单阶段漏斗:从合同拟定到发运的全流程追踪',
+        '{chapter3_title}': '区域与趋势',
+        '{chapter3_desc}': '区域分布、国家排名与30日追踪趋势',
+        '{chapter4_title}': '团队与展望',
+        '{chapter4_desc}': '团队绩效、支持需求与下月工作规划',
+    })
+
+    nav_labels = ['月度总览', '订单状态', '区域趋势', '团队展望']
+
+    # Page 3: Monthly Overview
+    s3 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s3, {
+        '{report_title}': '海外订单月度数据报告',
+        '{date}': period_str,
+        '{page_title}': f"{month}月核心指标:累计追踪{metrics['total_contracts']:,}单,覆盖{metrics['total_qty']:,}台车辆",
+        '{source}': source,
+        '{period}': '3/11',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s3, nav_labels, active_index=0, slide_width=prs.slide_width)
+    kpis = [
+        {'label': '合同总数', 'value': f"{metrics['total_contracts']:,}", 'unit': '单',
+         'change': f'日均{metrics["avg_daily_orders"]:.0f}单', 'sub': '覆盖全月'},
+        {'label': '车辆总数', 'value': f"{metrics['total_qty']:,}", 'unit': '台',
+         'change': f"{metrics['shipped_qty']:,}台已发运", 'sub': '交付持续推进'},
+        {'label': f'{month}月新签', 'value': metrics['new_contracts'], 'unit': '单',
+         'change': f"{metrics['new_qty']:,}台", 'sub': '新签势头良好'},
+        {'label': '已发运', 'value': metrics['shipped_orders'], 'unit': '单',
+         'change': f"{metrics['shipped_qty']:,}台", 'sub': '交付稳步推进'},
+        {'label': '目的国', 'value': f"{metrics['countries']}+", 'unit': '个',
+         'change': '全球市场布局', 'sub': '持续深化'},
+        {'label': '待处理需求', 'value': metrics['support_count'], 'unit': '单',
+         'change': f"{metrics['support_pct']}%订单涉及", 'sub': '需跨部门协调'},
+    ]
+    _add_kpi_cards(s3, kpis, start_y=Emu(content_top))
+
+    # Monthly overview: KPI cards only, no bottom insight text to avoid overlap with cards
+
+    # Page 4: Status Funnel
+    s4 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s4, {
+        '{report_title}': '海外订单月度数据报告',
+        '{date}': period_str,
+        '{page_title}': '订单阶段漏斗:合同拟定与生产中订单占主导地位',
+        '{source}': source,
+        '{period}': '4/11',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s4, nav_labels, active_index=1, slide_width=prs.slide_width)
+    funnel = metrics.get('status_funnel', {})
+    if funnel:
+        names = list(funnel.keys())
+        orders = [v['orders'] for v in funnel.values()]
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+        add_funnel_chart(s4, names, orders,
+                         Emu(762000), Emu(content_top), chart_w, Emu(5334000),
+                         show_data_labels=True, show_percent=True)
+        # Deep insight: monthly funnel
+        insight_items = generate_deep_insights('monthly', 'monthly_funnel', metrics)
+        _add_structured_insight(s4, insight_items,
+                                text_left, Emu(content_top), text_w, Emu(5334000))
+
+    # Page 5: Region Distribution
+    s5 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s5, {
+        '{report_title}': '海外订单月度数据报告',
+        '{date}': period_str,
+        '{page_title}': '区域分布:拉美、东南亚、非洲三大市场并驾齐驱',
+        '{source}': source,
+        '{period}': '5/11',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s5, nav_labels, active_index=2, slide_width=prs.slide_width)
+    rdist = metrics.get('region_dist', {})
+    if rdist:
+        regions = list(rdist.keys())
+        qtys = [v['qty'] for v in rdist.values()]
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+        add_doughnut_chart(s5, regions, qtys,
+                           Emu(762000), Emu(content_top), chart_w, Emu(5334000),
+                           show_legend=True, show_data_labels=True, show_percent=True,
+                           ring_ratio=0.6)
+        # Deep insight: monthly region
+        insight_items = generate_deep_insights('monthly', 'monthly_region', metrics)
+        _add_structured_insight(s5, insight_items,
+                                text_left, Emu(content_top), text_w, Emu(5334000))
+
+    # Page 6: TOP10 Countries
+    s6 = _duplicate_slide(prs, prs.slides[1])
+    top10 = metrics.get('top_countries', {})
+    top_country = list(top10.keys())[0] if top10 else ''
+    top_qty = list(top10.values())[0]['qty'] if top10 else 0
+    top10_title = f'Top 10目的国:{top_country}{top_qty:,}台领跑' if top_country else 'Top 10目的国:重点市场领跑'
+    _replace_all_placeholders(s6, {
+        '{report_title}': '海外订单月度数据报告',
+        '{date}': period_str,
+        '{page_title}': top10_title,
+        '{source}': source,
+        '{period}': '6/11',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s6, nav_labels, active_index=2, slide_width=prs.slide_width)
+    top10 = metrics.get('top_countries', {})
+    if top10:
+        countries = list(top10.keys())
+        qtys = [v['qty'] for v in top10.values()]
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+        add_horizontal_bar_chart(s6, countries, qtys,
+                                 Emu(762000), Emu(content_top), chart_w, Emu(4826000),
+                                 series_name='订单量(台)', color=C_ACCENT, reverse_order=True,
+                                 value_axis_title='订单量(台)')
+        # Deep insight: monthly top countries
+        insight_items = generate_deep_insights('monthly', 'monthly_country', metrics)
+        _add_structured_insight(s6, insight_items,
+                                text_left, Emu(content_top), text_w, Emu(4826000))
+
+    # Page 7: 30-Day Trend
+    s7 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s7, {
+        '{report_title}': '海外订单月度数据报告',
+        '{date}': period_str,
+        '{page_title}': '30日追踪趋势:下旬订单活跃度显著提升',
+        '{source}': source,
+        '{period}': '7/11',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s7, nav_labels, active_index=2, slide_width=prs.slide_width)
+    trend = metrics.get('daily_trend', {})
+    chart_w = Emu(int(prs.slide_width) * 0.55)
+    text_left = Emu(int(prs.slide_width) * 0.62)
+    text_w = Emu(int(prs.slide_width) * 0.36)
+    if trend:
+        dates = list(trend.keys())
+        vals = list(trend.values())
+        add_line_chart(s7, dates, vals,
+                       Emu(762000), Emu(content_top), chart_w, Emu(5334000),
+                       series_name='订单量', color=C_ACCENT,
+                       category_axis_title='日期(MM/DD)', value_axis_title='订单数')
+    tbp = metrics.get('trend_by_period', {})
+    late_change = tbp.get('late_change_pct', 0)
+    late_change_str = _format_pct(late_change, with_sign=True) if late_change is not None else '—'
+
+    # Deep insight: monthly trend
+    insight_items = generate_deep_insights('monthly', 'monthly_trend', metrics)
+    _add_structured_insight(s7, insight_items,
+                            text_left, Emu(content_top), text_w, Emu(5334000))
+
+    # Page 8: Team Performance
+    s8 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s8, {
+        '{report_title}': '海外订单月度数据报告',
+        '{date}': period_str,
+        '{page_title}': '团队绩效:9位负责人均匀分布,多人领跑',
+        '{source}': source,
+        '{period}': '8/11',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s8, nav_labels, active_index=3, slide_width=prs.slide_width)
+    team = metrics.get('team', {})
+    if team:
+        names = list(team.keys())
+        orders = [v['orders'] for v in team.values()]
+        qtys = [v['qty'] for v in team.values()]
+        chart_w = Emu(int(prs.slide_width) * 0.55)
+        text_left = Emu(int(prs.slide_width) * 0.62)
+        text_w = Emu(int(prs.slide_width) * 0.36)
+        # Horizontal bar chart for orders + secondary series for qty
+        add_grouped_bar_chart(s8, names,
+                              [('订单数', orders), ('车辆数', qtys)],
+                              Emu(762000), Emu(content_top), chart_w, Emu(4826000),
+                              colors=[C_ACCENT, C_ORANGE],
+                              show_legend=True, show_data_labels=True,
+                              category_axis_title='负责人', value_axis_title='数量')
+        # Deep insight: monthly team
+        insight_items = generate_deep_insights('monthly', 'monthly_team', metrics)
+        _add_structured_insight(s8, insight_items,
+                                text_left, Emu(content_top), text_w, Emu(4826000))
+
+    # Page 9: Support Analysis
+    s9 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s9, {
+        '{report_title}': '海外订单月度数据报告',
+        '{date}': period_str,
+        '{page_title}': '支持需求分析:财务、售后、法务为三大核心诉求',
+        '{source}': source,
+        '{period}': '9/11',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s9, nav_labels, active_index=3, slide_width=prs.slide_width)
+    sc = metrics.get('support_categories', {})
+    if sc:
+        cats = list(sc.keys())
+        vals = list(sc.values())
+        add_horizontal_bar_chart(s9, cats, vals,
+                                 Emu(762000), Emu(content_top), Emu(8636000), Emu(5334000),
+                                 series_name='需求数', color=C_ACCENT, reverse_order=True,
+                                 value_axis_title='需求数')
+        top_cat = max(sc.items(), key=lambda x: x[1])
+        # Deep insight: monthly support
+        insight_items = generate_deep_insights('monthly', 'monthly_support', metrics)
+        _add_structured_insight(s9, insight_items,
+                                Emu(9779000), Emu(content_top), Emu(5715000), Emu(5334000))
+
+    # Page 10: Next Month Plan
+    s10 = _duplicate_slide(prs, prs.slides[1])
+    _replace_all_placeholders(s10, {
+        '{report_title}': '海外订单月度数据报告',
+        '{date}': period_str,
+        '{page_title}': f'{month+1 if month < 12 else 1}月展望:预测交付{metrics["forecast_next"]}台,重点关注交付转化',
+        '{source}': source,
+        '{period}': '10/11',
+        '{page_num}': '',
+    })
+    _add_nav_tabs(s10, nav_labels, active_index=3, slide_width=prs.slide_width)
+
+    # Left chart: next month goals as column chart
+    goals = metrics.get('next_month_goals', [])
+    chart_w = Emu(int(prs.slide_width) * 0.55)
+    text_left = Emu(int(prs.slide_width) * 0.62)
+    text_w = Emu(int(prs.slide_width) * 0.36)
+    if goals:
+        goal_names = [g['title'].split(':')[0] for g in goals[:5]]
+        goal_nums = [g.get('number', 0) for g in goals[:5]]
+        add_column_chart(s10, goal_names, goal_nums,
+                         Emu(762000), Emu(content_top), chart_w, Emu(4826000),
+                         series_name='目标数量', color=C_ACCENT,
+                         category_axis_title='目标', value_axis_title='数量')
+
+    # Deep insight: monthly plan
+    insight_items = generate_deep_insights('monthly', 'monthly_plan', metrics)
+    _add_structured_insight(s10, insight_items,
+                            text_left, Emu(content_top), text_w, Emu(5334000))
+
+    # Page 11: End
+    s_end = _duplicate_slide(prs, prs.slides[3])
+    _add_footer_if_missing(s_end, f'数据来源:{source} | 11/11')
+    _replace_all_placeholders(s_end, {
+        '{report_title}': '海外订单月度数据报告',
+        '{date}': period_str,
+        '{department}': department,
+    })
+    end_kpis = [
+        ('合同总数', f"{metrics['total_contracts']:,}"),
+        ('车辆总数', f"{metrics['total_qty']:,}"),
+        ('目的国家', f"{metrics['countries']}+"),
+        ('团队', '9人'),
+    ]
+    for i, (lbl, val) in enumerate(end_kpis, 1):
+        _replace_placeholder(s_end, f'{{kpi{i}_label}}', lbl)
+        _replace_placeholder(s_end, f'{{kpi{i}_value}}', val)
+
+    for slide in prs.slides:
+        _ensure_word_wrap_all(slide)
+    _delete_template_slides(prs)
+    prs.save(output_path)
+    print(f"Monthly report saved: {output_path}")
+
+
+# ==============================================================================
+# CLI
+# ==============================================================================
+
+if __name__ == '__main__':
+    import sys
+    if len(sys.argv) >= 4:
+        cmd = sys.argv[1]
+        data_file = sys.argv[2]
+        output = sys.argv[3]
+        if cmd == 'daily':
+            d = datetime.strptime(sys.argv[4], '%Y-%m-%d')
+            build_daily_report(data_file, d, output)
+        elif cmd == 'weekly':
+            build_weekly_report(data_file, int(sys.argv[4]), int(sys.argv[5]), output)
+        elif cmd == 'monthly':
+            build_monthly_report(data_file, int(sys.argv[4]), int(sys.argv[5]), output)

二進制
海外订单数据周报.pptx


二進制
海外订单数据日报.pptx


二進制
海外订单日报_4月数据.xlsx


二進制
海外订单月度数据报告(2026年4月).pptx