#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""宝锐生物 · 公司介绍（诊断原料销售新人培训）PPT 生成器 V01"""
from pathlib import Path
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
from pptx.chart.data import ChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION, XL_LABEL_POSITION
from pptx.oxml.ns import qn
from lxml import etree
from PIL import Image

BASE = Path('/Users/liuxinyuan/Desktop/Hermes输出-工作类')
OUTDIR = BASE / 'PPT撰写任务/诊断原料销售新人培训_20261102/04_公司介绍与产品解决方案'
OUT = OUTDIR / '宝锐生物_公司介绍_诊断原料新人培训_20261102_V02.pptx'
LOGO = Path('/Users/liuxinyuan/work/公司资料/logo_transparent.png')
COVER_IMG = Path('/tmp/company_imgs/slide01_1.jpg')
LOBBY_IMG = Path('/tmp/company_imgs/slide02_1.jpg')
VIAL_IMG = Path('/tmp/company_imgs/slide11_1.jpg')
BOX_IMG = Path('/tmp/company_imgs/slide15_1.jpg')

# 品牌色（沿用 2026 公司模板的蓝/青体系）
NAVY = RGBColor(0x0B, 0x3D, 0x62)
BLUE = RGBColor(0x1F, 0x4E, 0x79)
TEAL = RGBColor(0x2A, 0x9D, 0x8F)
LIGHT = RGBColor(0xEE, 0xF6, 0xFB)
PALE = RGBColor(0xF7, 0xFB, 0xFD)
INK = RGBColor(0x1C, 0x2B, 0x36)
GREY = RGBColor(0x5C, 0x7C, 0x90)
MID = RGBColor(0x8A, 0xA6, 0xB6)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
ORANGE = RGBColor(0xE8, 0x8B, 0x2E)

FONT_CN = '宋体'
FONT_EN = 'Times New Roman'


def set_run_font(run, size=12, bold=False, color=INK, italic=False):
    run.font.name = FONT_EN
    run.font.size = Pt(size)
    run.font.bold = bold
    run.font.italic = italic
    run.font.color.rgb = color
    rPr = run._r.get_or_add_rPr()
    ea = rPr.find(qn('a:ea'))
    if ea is None:
        ea = etree.SubElement(rPr, qn('a:ea'))
    ea.set('typeface', FONT_CN)


def set_para(p, size=12, bold=False, color=INK, align=PP_ALIGN.LEFT,
             space_after=4, line=1.15):
    p.alignment = align
    p.space_after = Pt(space_after)
    p.line_spacing = line
    for r in p.runs:
        set_run_font(r, size, bold, color)


def text(slide, x, y, w, h, s, size=12, bold=False, color=INK,
         align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, line=1.15,
         space_after=4, font=None):
    box = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
    tf = box.text_frame
    tf.clear()
    tf.word_wrap = True
    tf.margin_left = 0
    tf.margin_right = 0
    tf.margin_top = 0
    tf.margin_bottom = 0
    tf.vertical_anchor = anchor
    p = tf.paragraphs[0]
    p.alignment = align
    p.line_spacing = line
    p.space_after = Pt(space_after)
    r = p.add_run()
    r.text = s
    set_run_font(r, size, bold, color)
    if font:
        r.font.name = font
    return box


def bullets(slide, x, y, w, h, items, size=12, color=INK, bullet_color=TEAL, line=1.25):
    box = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
    tf = box.text_frame
    tf.clear()
    tf.word_wrap = True
    tf.margin_left = 0
    tf.margin_right = 0
    tf.margin_top = 0
    tf.margin_bottom = 0
    for i, item in enumerate(items):
        p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
        p.space_after = Pt(8)
        p.line_spacing = line
        r = p.add_run()
        r.text = '• '
        set_run_font(r, size, True, bullet_color)
        r2 = p.add_run()
        r2.text = item
        set_run_font(r2, size, False, color)
    return box


def rect(slide, x, y, w, h, fill, line=None, radius=False):
    shp = slide.shapes.add_shape(
        MSO_SHAPE.ROUNDED_RECTANGLE if radius else MSO_SHAPE.RECTANGLE,
        Inches(x), Inches(y), Inches(w), Inches(h)
    )
    shp.fill.solid()
    shp.fill.fore_color.rgb = fill
    if line is None:
        shp.line.fill.background()
    else:
        shp.line.color.rgb = line
        shp.line.width = Pt(0.75)
    return shp


def line(slide, x1, y1, x2, y2, color=TEAL, width=1.2):
    shp = slide.shapes.add_connector(1, Inches(x1), Inches(y1), Inches(x2), Inches(y2))
    shp.line.color.rgb = color
    shp.line.width = Pt(width)
    return shp


def add_header(slide, section, page):
    # 顶部细线
    rect(slide, 0, 0, 13.333, 0.07, NAVY)
    slide.shapes.add_picture(str(LOGO), Inches(0.55), Inches(0.22), width=Inches(1.25))
    text(slide, 9.55, 0.24, 3.2, 0.3, section, 8, False, GREY, PP_ALIGN.RIGHT)
    line(slide, 0.65, 7.08, 12.68, 7.08, RGBColor(0xD8, 0xE6, 0xEF), 0.7)
    text(slide, 0.65, 7.16, 7.0, 0.2, '宝锐生物 ｜ 专注原料开发 · 提供优质服务', 7.5, False, GREY)
    text(slide, 11.7, 7.16, 1.0, 0.2, f'{page:02d} / 18', 8, False, GREY, PP_ALIGN.RIGHT)


def add_title(slide, kicker, title, subtitle=None):
    text(slide, 0.65, 0.63, 11.8, 0.3, kicker, 9.5, True, TEAL)
    text(slide, 0.65, 0.95, 12.0, 0.55, title, 25, True, NAVY)
    line(slide, 0.65, 1.58, 12.68, 1.58, TEAL, 1.5)
    if subtitle:
        text(slide, 0.65, 1.72, 11.9, 0.35, subtitle, 11, False, GREY)


def add_source(slide, s):
    text(slide, 0.65, 6.78, 12.0, 0.22, s, 6.8, False, MID)


def new_slide(prs):
    s = prs.slides.add_slide(prs.slide_layouts[6])
    bg = s.background
    bg.fill.solid()
    bg.fill.fore_color.rgb = WHITE
    return s


def rounded_chip(slide, x, y, w, h, label, fill=LIGHT, color=NAVY, size=10, bold=True):
    shp = rect(slide, x, y, w, h, fill, None, True)
    text(slide, x + 0.12, y + 0.05, w - 0.24, h - 0.1, label, size, bold, color,
         PP_ALIGN.CENTER, MSO_ANCHOR.MIDDLE)


def add_table(slide, rows, x, y, w, h, col_widths=None, font_size=10,
              header=True, first_col_bold=True):
    shape = slide.shapes.add_table(len(rows), len(rows[0]), Inches(x), Inches(y), Inches(w), Inches(h))
    table = shape.table
    if col_widths:
        for i, cw in enumerate(col_widths):
            table.columns[i].width = Inches(cw)
    for r, row in enumerate(rows):
        for c, val in enumerate(row):
            cell = table.cell(r, c)
            cell.margin_left = Inches(0.08)
            cell.margin_right = Inches(0.06)
            cell.margin_top = Inches(0.04)
            cell.margin_bottom = Inches(0.03)
            cell.vertical_anchor = MSO_ANCHOR.MIDDLE
            cell.fill.solid()
            if header and r == 0:
                cell.fill.fore_color.rgb = BLUE
            elif r % 2 == 0:
                cell.fill.fore_color.rgb = RGBColor(0xF4, 0xF9, 0xFC)
            else:
                cell.fill.fore_color.rgb = WHITE
            tf = cell.text_frame
            tf.clear()
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.space_after = Pt(0)
            p.line_spacing = 1.0
            r0 = p.add_run()
            r0.text = str(val)
            set_run_font(r0, font_size, header and r == 0 or (first_col_bold and c == 0),
                         WHITE if header and r == 0 else (NAVY if c == 0 else INK))
    return table


def make_cover_image():
    img = Image.open(COVER_IMG).convert('RGB')
    w, h = img.size
    target = 6.0 / 7.5
    if w / h > target:
        new_w = int(h * target)
        left = (w - new_w) // 2
        img = img.crop((left, 0, left + new_w, h))
    else:
        new_h = int(w / target)
        top = 0
        img = img.crop((0, top, w, top + new_h))
    out = Path('/tmp/cover_building.jpg')
    img.resize((1200, 1500)).save(out, quality=92)
    return out


def add_bar_chart(slide, x, y, w, h, cats, series, colors, title='', number_format='0'):
    cd = ChartData()
    cd.categories = cats
    for name, vals in series:
        cd.add_series(name, vals)
    chart = slide.shapes.add_chart(XL_CHART_TYPE.COLUMN_CLUSTERED, Inches(x), Inches(y), Inches(w), Inches(h), cd).chart
    chart.has_legend = len(series) > 1
    if chart.has_legend:
        chart.legend.position = XL_LEGEND_POSITION.BOTTOM
        chart.legend.include_in_layout = False
        chart.legend.font.size = Pt(8)
        chart.legend.font.name = FONT_EN
    chart.font.name = FONT_EN
    chart.font.size = Pt(8)
    chart.font.color.rgb = INK
    chart.has_title = bool(title)
    if title:
        chart.chart_title.text_frame.text = title
        for p in chart.chart_title.text_frame.paragraphs:
            for r in p.runs:
                set_run_font(r, 10, True, NAVY)
    plot = chart.plots[0]
    plot.gap_width = 55
    plot.overlap = -8
    for i, s in enumerate(chart.series):
        s.format.fill.solid()
        s.format.fill.fore_color.rgb = colors[i % len(colors)]
    plot.has_data_labels = True
    dl = plot.data_labels
    dl.font.size = Pt(8)
    dl.font.name = FONT_EN
    dl.font.bold = True
    dl.number_format = '#,##0'
    dl.number_format_is_linked = False
    dl.position = XL_LABEL_POSITION.OUTSIDE_END
    chart.value_axis.has_major_gridlines = False
    chart.value_axis.visible = False
    chart.category_axis.tick_labels.font.size = Pt(8)
    chart.category_axis.tick_labels.font.name = FONT_EN
    return chart


def main():
    prs = Presentation()
    prs.slide_width = Inches(13.333)
    prs.slide_height = Inches(7.5)

    # 01 封面
    s = new_slide(prs)
    rect(s, 0, 0, 7.35, 7.5, PALE)
    rect(s, 0, 0, 0.12, 7.5, TEAL)
    s.shapes.add_picture(str(LOGO), Inches(0.65), Inches(0.45), width=Inches(1.7))
    text(s, 0.65, 1.75, 5.8, 0.35, '诊断原料销售新人培训', 11, True, TEAL)
    text(s, 0.65, 2.05, 6.2, 0.8, '公司介绍', 40, True, NAVY)
    text(s, 0.65, 3.0, 5.8, 0.8, '公司介绍和合作模式', 18, False, BLUE)
    text(s, 0.65, 4.05, 6.0, 1.3,
         '公司现在做什么，客户为什么会找我们。', 12, False, GREY, line=1.5)
    text(s, 0.65, 6.25, 5.5, 0.3, '2026 年 11 月 ｜ 珠海', 10, False, GREY)
    text(s, 0.65, 6.65, 5.6, 0.3, '守护生命健康，缔造美好生活', 9, False, MID)
    cover = make_cover_image()
    s.shapes.add_picture(str(cover), Inches(7.35), Inches(0), width=Inches(5.98), height=Inches(7.5))
    # 半透明深蓝压角，让图片与左侧留白衔接
    rect(s, 7.25, 0, 0.12, 7.5, NAVY)

    # 02 目录
    s = new_slide(prs); add_header(s, '公司介绍 / 新人培训', 2)
    add_title(s, '今天讲什么', '两小时，讲清公司现在做什么', '以及客户为什么会找我们')
    items = [
        ('01', '公司从哪里来', '2012 年成立，从核酸诊断原料起步'),
        ('02', '现在做什么', '诊断原料、生命科学、生物医药三条线'),
        ('03', '凭什么被信任', '资质、质量体系、技术平台'),
        ('04', '市场怎么变', 'IVD 总量、分子诊断、上游原料与酶'),
        ('05', '客户难在哪', '成本、研发效率、信息差；宝锐怎么接'),
    ]
    for i, (num, head, desc) in enumerate(items):
        y = 2.0 + i * 0.9
        text(s, 0.8, y, 0.5, 0.3, num, 15, True, TEAL)
        text(s, 1.35, y - 0.03, 3.1, 0.3, head, 15, True, NAVY)
        text(s, 4.45, y, 6.4, 0.3, desc, 10.5, False, GREY)
        line(s, 1.35, y + 0.42, 12.0, y + 0.42, RGBColor(0xDD, 0xEB, 0xF2), 0.7)

    # 03 成立背景
    s = new_slide(prs); add_header(s, '01 / 公司背景', 3)
    add_title(s, '01 / 公司背景', '2012 年，公司在珠海成立')
    text(s, 0.65, 1.9, 1.7, 1.0, '2012', 46, True, TEAL)
    text(s, 0.65, 3.0, 2.0, 0.5, '公司成立', 12, True, NAVY)
    text(s, 2.45, 1.95, 4.0, 1.4,
         '珠海宝锐生物科技有限公司成立。\n从核酸诊断试剂核心原料做起，先解决酶和预混液。', 11.5, False, INK, line=1.5)
    milestones = [
        ('2017', '国家高新技术企业'),
        ('2018', 'ISO 9001 体系认证'),
        ('2020', '南屏新厂投产；新冠核酸原料覆盖数十家企业'),
        ('2021', '广东省专精特新；研发生产中心扩建'),
        ('2022', 'mRNA 原料酶上市'),
    ]
    line(s, 2.25, 4.25, 12.0, 4.25, RGBColor(0xC9, 0xDE, 0xE9), 1.2)
    for i, (year, label) in enumerate(milestones):
        x = 2.25 + i * 2.22
        rect(s, x - 0.07, 4.18, 0.14, 0.14, TEAL, None, True)
        text(s, x - 0.1, 3.7, 0.65, 0.3, year, 13, True, NAVY)
        text(s, x - 0.1, 4.45, 2.0, 0.8, label, 9.5, False, GREY, line=1.25)
    text(s, 0.65, 5.55, 4.7, 0.8, '2012 年成立时，公司先做核酸诊断用酶和预混液；后来才扩展到三条业务线。', 12, False, BLUE, line=1.45)
    add_source(s, '来源：公司资料、公开报道；时间线按公司历史资料整理。')

    # 04 业务线
    s = new_slide(prs); add_header(s, '02 / 业务布局', 4)
    add_title(s, '02 / 业务布局', '三条业务线，共用一套酶工程平台')
    rows = [
        ['业务线', '主要产品', '服务客户'],
        ['诊断原料', '分子酶、qPCR/RT-qPCR/LAMP 预混、核酸提取、冻干保护剂', 'IVD、POCT、病原检测'],
        ['生命科学', '工具酶、分子试剂、核酸提取纯化、测序试剂、蛋白研究', '高校、科研院所、CRO'],
        ['生物医药', 'mRNA 原料酶、加帽酶、DNase I、RNase H II、生物药 QC', 'mRNA 药物、生物药'],
    ]
    tbl = add_table(s, rows, 0.65, 1.85, 12.0, 3.25, [1.55, 7.4, 3.0], 11, True)
    tbl.rows[0].height = Inches(0.42)
    for _r in list(tbl.rows)[1:]:
        _r.height = Inches(0.78)
    rect(s, 0.65, 5.5, 12.0, 0.7, LIGHT, None, True)
    text(s, 0.9, 5.65, 11.5, 0.3,
         '共用底座：蛋白进化 · 重组表达 · 抗体/适配体 · 工艺开发 · 质量体系', 10.5, True, NAVY)
    text(s, 0.65, 6.35, 11.8, 0.3, '三条线共用同一套酶工程平台。', 10, False, GREY)

    # 05 技术平台
    s = new_slide(prs); add_header(s, '02 / 技术平台', 5)
    add_title(s, '02 / 技术平台', '公司自己完成酶分子的发现、改造和量产')
    cards = [
        ('蛋白进化', 'MTPS 和 MHTS 筛选\n提高活性和稳定性'),
        ('重组表达', '工程菌株、三级种子库\n高密度发酵、精细纯化'),
        ('抗体开发', '多克隆/单克隆抗体\n免疫原设计与筛选'),
        ('适配体筛选', 'SELEX 筛选\n化学/配基修饰'),
        ('工艺开发', '配方、冻干、放大\n批间一致性'),
        ('应用研究', 'qPCR、LAMP、直扩\n客户体系验证'),
    ]
    for i, (head, body) in enumerate(cards):
        col = i % 3; row = i // 3
        x = 0.65 + col * 4.12
        y = 1.95 + row * 2.15
        rect(s, x, y, 3.85, 1.85, PALE, RGBColor(0xD8, 0xE6, 0xEF), True)
        rect(s, x, y, 0.08, 1.85, TEAL)
        text(s, x + 0.25, y + 0.18, 3.3, 0.3, head, 14, True, NAVY)
        text(s, x + 0.25, y + 0.65, 3.35, 0.85, body, 9.5, False, GREY, line=1.35)
    text(s, 0.65, 6.35, 11.6, 0.3, '客户拿到的是酶、缓冲和冻干工艺配好的一套东西。', 10.5, True, TEAL)

    # 06 资质
    s = new_slide(prs); add_header(s, '03 / 资质与质量', 6)
    add_title(s, '03 / 资质与质量', '客户审厂，先翻资质')
    quals = [
        ('ISO 9001:2015', '质量管理体系'),
        ('ISO 13485:2016', '医疗器械质量管理体系'),
        ('国家高新技术企业', '高新技术企业认定'),
        ('广东省专精特新中小企业', '广东省专精特新'),
        ('广东省分子诊断原料酶工程技术研究中心', '省级工程技术研究中心'),
        ('珠海市企业技术中心', '市级企业技术中心'),
        ('广东省博士后创新实践基地', '博士后创新实践基地'),
        ('国家专精特新“小巨人”企业', '专精特新“小巨人”'),
    ]
    for i, (name, sub) in enumerate(quals):
        col = i % 4; row = i // 4
        x = 0.65 + col * 3.08
        y = 1.95 + row * 1.85
        rect(s, x, y, 2.85, 1.55, PALE, RGBColor(0xD8, 0xE6, 0xEF), True)
        rect(s, x, y, 0.07, 1.55, TEAL)
        text(s, x + 0.2, y + 0.18, 2.45, 0.55, name, 10.5, True, NAVY, line=1.15)
        text(s, x + 0.2, y + 0.9, 2.45, 0.25, sub, 8.5, False, GREY)
    text(s, 0.65, 5.95, 12.0, 0.4, '客户问资质，别只报名字。ISO 13485 是医疗器械的入场券，ISO 9001 是通用质量体系；说得出区别，才算答上来。', 10.5, False, INK)
    add_source(s, '来源：公司资质文件合订本；按公司现有资质罗列。')

    # 07 市场漏斗
    s = new_slide(prs); add_header(s, '04 / 市场分析', 7)
    add_title(s, '04 / 市场分析', 'IVD、分子诊断、原料、酶：四层市场', '每一层的钱，花在哪里')
    bars = [
        ('中国 IVD 市场', '1,080 亿元', 1.95, 11.4, NAVY, '2025 实际'),
        ('中国分子诊断市场', '319 亿元 E', 2.65, 9.6, BLUE, '2025E'),
        ('中国 IVD 原料市场', '228 亿元 E', 3.35, 7.8, TEAL, '2025E'),
        ('中国分子诊断原料酶', '52 亿元 E', 4.05, 6.0, RGBColor(0x78, 0xB9, 0x4A), '2025E'),
    ]
    for i, (label, val, y, w, col, year) in enumerate(bars):
        x = (13.333 - w) / 2
        rect(s, x, y, w, 0.58, col)
        text(s, x + 0.22, y + 0.08, w - 2.2, 0.28, label, 10.8, True, WHITE)
        text(s, x + 0.22, y + 0.33, w - 2.2, 0.2, year, 7.2, False, RGBColor(0xE6, 0xF2, 0xF8))
        text(s, x + w - 1.85, y + 0.115, 1.6, 0.3, val, 11.5, True, WHITE, PP_ALIGN.RIGHT)
        text(s, 0.65, 5.55, 11.8, 0.55, '各层统计口径不同，本页不做比例换算；看的是价值量逐层收窄。', 9.5, False, GREY)
    add_source(s, '来源：中国医疗器械行业协会体外诊断分会《中国体外诊断行业年度报告（2021–2025）》；Frost & Sullivan；2025E 为预测值。')

    # 08 IVD 总量
    s = new_slide(prs); add_header(s, '04 / 市场分析', 8)
    add_title(s, '04 / 市场分析', '终端在收缩，价格压力还在')
    add_bar_chart(s, 0.65, 1.85, 7.8, 4.55, ['2021', '2022', '2023', '2024', '2025'],
                  [('中国 IVD 市场规模（亿元）', [1300, 1700, 1200, 1200, 1080])],
                  [BLUE], '')
    text(s, 8.75, 1.95, 3.8, 0.3, '2022 年峰值', 9, True, TEAL)
    text(s, 8.75, 2.2, 3.8, 0.7, '1,700 亿元', 28, True, BLUE)
    text(s, 8.75, 2.95, 3.8, 0.3, '2025 年', 9, True, TEAL)
    text(s, 8.75, 3.2, 3.8, 0.7, '1,080 亿元', 28, True, NAVY)
    text(s, 8.75, 4.1, 3.4, 0.8, '较峰值回落约 36%\n集采、DRG/DIP 继续压价。', 10.5, False, INK, line=1.4)
    add_source(s, '来源：中国医疗器械行业协会体外诊断分会《中国体外诊断行业年度报告（2021–2025）》。')

    # 09 上游增长
    s = new_slide(prs); add_header(s, '04 / 市场分析', 9)
    add_title(s, '04 / 市场分析', '上游还在长，但增速已经降下来')
    add_bar_chart(s, 0.65, 1.85, 7.4, 4.55, ['IVD 原料', '分子诊断原料酶'],
                  [('2020', [104, 32]), ('2025E', [228, 52])],
                  [MID, TEAL], '')
    text(s, 8.35, 1.9, 4.3, 0.3, 'IVD 原料', 9, True, TEAL)
    text(s, 8.35, 2.2, 4.3, 0.6, '104 → 228 亿元', 18, True, NAVY)
    text(s, 8.35, 2.8, 4.3, 0.3, '2020–2025E 年复合增速 17.0%', 9.5, False, GREY)
    text(s, 8.35, 3.45, 4.3, 0.3, '分子诊断原料酶', 9, True, TEAL)
    text(s, 8.35, 3.75, 4.3, 0.6, '32 → 52 亿元', 18, True, NAVY)
    text(s, 8.35, 4.35, 4.3, 0.3, '2020–2025E 年复合增速 10.0%', 9.5, False, GREY)
    add_source(s, '来源：Frost & Sullivan；2025E 为预测值，单位：亿元人民币。')

    # 10 竞争
    s = new_slide(prs); add_header(s, '04 / 市场分析', 10)
    add_title(s, '04 / 市场分析', '进口占大头，国产在往上走')
    # 用形状画条形图，避免某些渲染器丢失原生图表预览
    text(s, 0.65, 1.86, 7.0, 0.3, '2021 年国产厂商内部份额', 10, True, NAVY)
    shares = [('诺唯赞', 63.42), ('菲鹏', 20.52), ('翌圣', 10.81),
              ('康为', 4.17), ('近岸', 0.89), ('其他', 0.20)]
    max_v = shares[0][1]
    for i, (name, val) in enumerate(shares):
        yy = 2.3 + i * 0.58
        text(s, 0.65, yy, 1.25, 0.28, name, 9.5, False, INK)
        bw = 4.5 * (val / max_v)
        rect(s, 2.0, yy + 0.03, max(bw, 0.06), 0.26, TEAL if i == 0 else BLUE)
        text(s, 2.0 + max(bw, 0.06) + 0.12, yy, 1.1, 0.28, f'{val:.1f}%', 8.5, True, NAVY)
    text(s, 8.0, 1.95, 4.5, 0.3, '全市场', 9, True, TEAL)
    text(s, 8.0, 2.25, 4.5, 0.8, '进口仍占约 80%', 24, True, NAVY)
    text(s, 8.0, 3.2, 4.5, 1.2, '进口占约 80%，国产厂商分剩下的 20%。\n客户换供应商时，先看批间稳定和应用支持。', 10.5, False, INK, line=1.45)
    text(s, 0.65, 6.3, 11.8, 0.3, '国产厂商内部份额不等于全市场份额；不要把 63.4% 当成诺唯赞在全市场的份额。', 9.5, False, GREY)
    add_source(s, '来源：Frost & Sullivan（2021，国产厂商内部份额）；华经产业研究院（2025，进口占比约 80%）。')

    # 11 三大痛点
    s = new_slide(prs); add_header(s, '05 / 客户痛点与宝锐合作', 11)
    add_title(s, '05 / 客户痛点与宝锐合作', '客户卡在哪')
    pains = [
        ('01', '成本', '集采、DRG/DIP 压价，\n试剂厂先扛，原料端跟着扛。'),
        ('02', '研发效率', '原料筛选反复试，\n项目做完，数据难复用。'),
        ('03', '信息差', '客户做 FFPE，原料厂只给酶参数；\n客户不知道怎么配。'),
    ]
    for i, (num, head, body) in enumerate(pains):
        x = 0.65 + i * 4.12
        rect(s, x, 2.0, 3.85, 3.4, PALE, RGBColor(0xD8, 0xE6, 0xEF), True)
        rect(s, x, 2.0, 3.85, 0.08, TEAL)
        text(s, x + 0.25, 2.25, 0.6, 0.4, num, 20, True, TEAL)
        text(s, x + 0.25, 2.8, 3.2, 0.4, head, 17, True, NAVY)
        text(s, x + 0.25, 3.45, 3.25, 1.4, body, 10.5, False, INK, line=1.5)
    # 不在页尾做总结

    # 12 痛点一
    s = new_slide(prs); add_header(s, '05 / 客户痛点与宝锐合作', 12)
    add_title(s, '05 / 客户痛点与宝锐合作', '第一道坎：成本')
    text(s, 0.65, 1.9, 3.2, 0.9, '集采 + DRG/DIP', 24, True, NAVY)
    text(s, 0.65, 2.8, 4.2, 2.8,
         '终端价格往下走，试剂厂先扛；\n试剂厂再往上游压，原料端跟着扛。\n\n做血筛的客户，换一批原料要重新做验证。\n便宜但换批不稳，不是降本。', 12, False, INK, line=1.55)
    rect(s, 5.2, 1.9, 7.3, 3.9, LIGHT, None, True)
    text(s, 5.5, 2.15, 6.7, 0.3, '宝锐能做的', 10, True, TEAL)
    bullets(s, 5.5, 2.55, 6.5, 2.8, [
        '按客户体系定制酶，减少不必要的组分和工艺。',
        '配方和冻干一起看，少做无效试验。',
        '批间稳定、供应稳定，减少客户换批验证的隐性成本。',
    ], 11, INK, TEAL)
    text(s, 0.65, 6.2, 11.8, 0.3, '报价前先算两笔账：原料单价，换批验证。', 11, True, TEAL)

    # 13 痛点二
    s = new_slide(prs); add_header(s, '05 / 客户痛点与宝锐合作', 13)
    add_title(s, '05 / 客户痛点与宝锐合作', '第二道坎：研发效率')
    text(s, 0.65, 1.9, 4.0, 0.8, '一半以上的工时，花在筛原料', 22, True, NAVY)
    text(s, 0.65, 3.0, 4.4, 2.5,
         '一个项目一套体系，酶和缓冲都要试；\n项目结束后，数据留在个人手里，\n下一个项目重新来一遍。', 12, False, INK, line=1.55)
    rect(s, 5.3, 1.9, 7.2, 3.9, LIGHT, None, True)
    text(s, 5.6, 2.15, 6.6, 0.3, '宝锐能做的', 10, True, TEAL)
    bullets(s, 5.6, 2.55, 6.4, 2.8, [
        '平台化酶库，先把大方向缩到少数几个候选。',
        '把应用数据一起给，不只给参数表。',
        '定制开发按项目沉淀，下一个项目能接着用。',
    ], 11, INK, TEAL)
    text(s, 0.65, 6.2, 11.8, 0.3, '下次客户说项目慢，先问：卡在筛酶，还是卡在验证方案？', 11, True, TEAL)

    # 14 痛点三
    s = new_slide(prs); add_header(s, '05 / 客户痛点与宝锐合作', 14)
    add_title(s, '05 / 客户痛点与宝锐合作', '第三道坎：信息差')
    text(s, 0.65, 1.9, 3.2, 0.8, '上下游，互相看不见', 22, True, NAVY)
    text(s, 0.65, 3.0, 4.4, 2.5,
         '做 FFPE 的客户，前端提取和扩增要一起看；\n原料厂只给酶参数，客户不知道怎么配。\n\n新项目立项慢，验证方案来回改。', 12, False, INK, line=1.55)
    rect(s, 5.3, 1.9, 7.2, 3.9, LIGHT, None, True)
    text(s, 5.6, 2.15, 6.6, 0.3, '宝锐能做的', 10, True, TEAL)
    bullets(s, 5.6, 2.55, 6.4, 2.8, [
        '销售、技术一起进客户现场，先看体系，不先报货号。',
        '把客户的指标翻译成酶的参数，联合定义验证方案。',
        '样品和数据一起给，缩短客户从立项到验证的时间。',
    ], 11, INK, TEAL)
    text(s, 0.65, 6.2, 11.8, 0.3, '第一次拜访，销售和技术一起去；先看反应体系，别先报货号。', 11, True, TEAL)

    # 15 合作模式
    s = new_slide(prs); add_header(s, '05 / 客户痛点与宝锐合作', 15)
    add_title(s, '05 / 客户痛点与宝锐合作', '合作模式：分子酶定制化',
              '客户给体系，我们配酶和缓冲')
    steps = [
        ('01', '问清体系', '客户现在跑什么体系，样本是什么'),
        ('02', '挑酶、改酶', '从酶库先挑几个候选，必要时改造'),
        ('03', '在客户体系里试', '用客户样本做灵敏度和稳定性'),
        ('04', '放大到生产', '定配方、冻干程序，跑批间一致'),
        ('05', '按批供货', '批批检，出 COA'),
    ]
    for i, (num, head, body) in enumerate(steps):
        x = 0.65 + i * 2.48
        rect(s, x, 2.25, 2.1, 2.9, PALE, RGBColor(0xD8, 0xE6, 0xEF), True)
        text(s, x + 0.18, 2.45, 0.5, 0.3, num, 15, True, TEAL)
        text(s, x + 0.18, 2.9, 1.75, 0.5, head, 11.5, True, NAVY, line=1.2)
        text(s, x + 0.18, 3.55, 1.75, 1.2, body, 8.8, False, GREY, line=1.35)
        if i < 4:
            text(s, x + 2.12, 3.45, 0.3, 0.3, '→', 18, True, TEAL)
    text(s, 0.65, 5.65, 11.8, 0.6, '项目开始时，先把客户的体系、样本问清楚。', 11, True, TEAL)

    # 16 场景化方案
    s = new_slide(prs); add_header(s, '05 / 客户痛点与宝锐合作', 16)
    add_title(s, '05 / 客户痛点与宝锐合作', '场景化方案：按客户体系配')
    cases = [
        ('高灵敏 qPCR / RT-qPCR', '低拷贝和抗抑制', '酶和缓冲一起调，探针按客户体系选'),
        ('LAMP / POCT', '恒温和冻干', 'Bst/RT 酶配冻干保护剂，程序一起定'),
        ('直扩 / 复杂样本', '血浆和植物样本', '耐抑制酶 + 样本兼容性验证'),
    ]
    for i, (head, tag, body) in enumerate(cases):
        x = 0.65 + i * 4.12
        rect(s, x, 1.95, 3.85, 3.7, PALE, RGBColor(0xD8, 0xE6, 0xEF), True)
        rect(s, x, 1.95, 3.85, 0.07, TEAL)
        text(s, x + 0.25, 2.2, 3.3, 0.65, head, 13.5, True, NAVY, line=1.2)
        text(s, x + 0.25, 2.95, 3.25, 0.3, tag, 10, True, TEAL)
        text(s, x + 0.25, 3.45, 3.3, 1.6, body, 10.5, False, INK, line=1.5)
    text(s, 0.65, 6.0, 11.8, 0.35, '客户跑 qPCR，就按 qPCR 配；跑 LAMP，就按 LAMP 配。', 10.5, True, TEAL)

    # 17 冻干与案例
    s = new_slide(prs); add_header(s, '05 / 客户痛点与宝锐合作', 17)
    add_title(s, '05 / 客户痛点与宝锐合作', '冻干服务：把液体试剂变成可运输的产品')
    text(s, 0.65, 1.9, 4.2, 0.3, '服务流程', 10, True, TEAL)
    steps = ['看配方', '定程序', '做小试', '跑稳定性', '放大生产']
    for i, step in enumerate(steps):
        y = 2.35 + i * 0.72
        rect(s, 0.65, y, 3.5, 0.5, LIGHT if i < 4 else RGBColor(0xE6, 0xF4, 0xEF), None, True)
        text(s, 0.85, y + 0.11, 3.1, 0.25, step, 10.5, True, NAVY)
        if i < 4:
            text(s, 4.0, y + 0.12, 0.4, 0.3, '↓', 14, True, TEAL)
    text(s, 4.55, 1.9, 8.0, 0.3, '验证例子', 10, True, TEAL)
    rows = [
        ['产品', '体系/样本', '结果'],
        ['M2071', 'ASFV 质粒', '3 copies/T，100% 检出，Ct CV 2.33%'],
        ['MD2071', 'HBV 血浆直扩', '25% 加样，23 分钟完成扩增'],
        ['FM5071', '人基因组四重检测', '45℃ 90 天、常温 1 年稳定性验证'],
    ]
    add_table(s, rows, 4.55, 2.25, 7.95, 2.7, [0.95, 2.1, 4.9], 9.5, True)
    text(s, 4.55, 5.25, 7.9, 0.6, '数据来自产品验证资料；具体体系以客户自己的验证结果为准。', 8.8, False, GREY)
    rect(s, 0.65, 5.95, 3.5, 0.62, LIGHT, None, True)
    text(s, 0.82, 6.08, 3.2, 0.3, '冻干要同时定配方、程序、保护剂和包装。', 8.8, False, NAVY)

    # 18 结尾
    s = new_slide(prs); add_header(s, '公司介绍 / 新人培训', 18)
    rect(s, 0, 0, 7.4, 7.5, PALE)
    text(s, 0.65, 1.3, 6.2, 0.4, '第一次拜访，先问清楚', 20, True, NAVY)
    questions = ['现在用的是什么体系？', '卡在哪一步：成本、性能，还是验证？', '下一个项目什么时候上？']
    for i, q in enumerate(questions):
        y = 2.1 + i * 0.85
        rect(s, 0.65, y, 0.45, 0.45, TEAL, None, True)
        text(s, 0.65, y + 0.09, 0.45, 0.25, str(i + 1), 10, True, WHITE, PP_ALIGN.CENTER)
        text(s, 1.3, y + 0.02, 5.7, 0.4, q, 12, False, INK)
    text(s, 0.65, 5.0, 6.2, 0.8, '第一次拜访回来，把客户体系和卡点写进跟进记录。', 20, True, NAVY, line=1.35)
    text(s, 0.65, 6.4, 6.0, 0.3, '刘新元 ｜ 诊断原料销售', 9.5, True, TEAL)
    text(s, 0.65, 6.7, 6.2, 0.25, 'marketing@biori.com ｜ 0756-8699969 ｜ www.biori.com', 7.5, False, GREY)
    s.shapes.add_picture(str(LOBBY_IMG), Inches(7.4), Inches(0), width=Inches(5.93), height=Inches(7.5))

    OUTDIR.mkdir(parents=True, exist_ok=True)
    prs.save(str(OUT))
    print('written', OUT, OUT.stat().st_size)


if __name__ == '__main__':
    main()
